feat(secretaria): roteamento por especialidade, situações, dúvida→humano, imagem, checklist do dono e desligar global

Secretária IA mais segura e contextual:
- Roteamento por especialidade: /contexto (dentistas+especialidades) injetado no
  cérebro; descobrir o motivo e priorizar clínico geral só p/ avaliação/limpeza.
- Situações da clínica: regras editáveis injetadas no prompt (siga à risca).
- Dúvida/caso incerto → tool encaminhar_humano (não agenda; pede p/ aguardar).
- Imagem/áudio: ext:message.transcription religa no fluxo ext:message.new
  (guard hadCaption evita processar 2x); a IA reage à descrição da imagem.
- Checklist do dono × banco → cautelas no /contexto: NÃO assumir clínico geral
  por padrão p/ quem já é paciente; sem certeza de quem/qual dentista → humano.
- Desligar GLOBAL da Secretária: GET/POST /secretaria/power (kill-switch
  auto_reply via config.set, efeito imediato, sem restart; visível a todos).

Inclui a plumbing da sessão (chat.upsert, ownership de conexão, ws-bridge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-06 15:40:26 +02:00
parent f9e93647bb
commit eca65bf765
16 changed files with 344 additions and 6 deletions
@@ -149,6 +149,13 @@ export class ProtocolEngine {
const r = await fetch(u, { headers: { 'x-nw-agenda-secret': agendaSecret ?? '' }, signal: AbortSignal.timeout(6000) })
if (r.ok) systemPrompt += ProtocolEngine.formatClinicStatus(await r.json())
} catch { /* best-effort: sem status, a Secretária segue com a data já injetada */ }
// Contexto clínico: dentistas+especialidades + situações (regras) + roteamento.
try {
const uc = new URL(`${agendaUrl.replace(/\/$/, '')}/contexto`)
uc.searchParams.set('clinica_id', statusClinicaId)
const rc = await fetch(uc, { headers: { 'x-nw-agenda-secret': agendaSecret ?? '' }, signal: AbortSignal.timeout(6000) })
if (rc.ok) systemPrompt += ProtocolEngine.formatClinicContext(await rc.json())
} catch { /* best-effort */ }
}
let response: string
@@ -216,6 +223,33 @@ export class ProtocolEngine {
return `${p(now.getDate())}${p(now.getMonth() + 1)}${String(now.getFullYear()).slice(-2)}${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`
}
// Formata o /contexto (dentistas+especialidades + situações/regras) p/ o prompt.
// Guia o roteamento por especialidade, as regras da clínica e o encaminhamento
// de dúvidas para a secretária humana.
static formatClinicContext(ctx: any): string {
const L: string[] = []
if (Array.isArray(ctx?.dentistas) && ctx.dentistas.length) {
L.push('=== DENTISTAS E ESPECIALIDADES ===')
for (const d of ctx.dentistas) L.push(`- ${d.nome}: ${(d.especialidades || []).join(', ') || 'clínico geral'}`)
L.push('ROTEAMENTO: descubra SEMPRE o motivo da consulta. Para avaliação, limpeza, restauração e dúvidas → priorize um CLÍNICO GERAL. Para casos específicos (ortodontia, endodontia/canal, prótese, implante etc.) → o dentista da ESPECIALIDADE.')
}
if (Array.isArray(ctx?.situacoes) && ctx.situacoes.length) {
L.push('')
L.push('=== SITUAÇÕES E REGRAS DA CLÍNICA (siga à risca) ===')
for (const s of ctx.situacoes) L.push(`- ${s}`)
}
if (Array.isArray(ctx?.cautelas) && ctx.cautelas.length) {
L.push('')
L.push('=== CAUTELAS (o sistema ainda não tem todos os dados — NÃO chute) ===')
for (const c of ctx.cautelas) L.push(`- ${c}`)
L.push('Na dúvida sobre QUEM é o paciente, QUAL dentista atende, ou O QUE ele quer: NÃO agende — use encaminhar_humano e peça para aguardar.')
}
L.push('')
L.push('DÚVIDAS / CASOS INCERTOS: se for uma dúvida inicial, um caso que você não sabe resolver, ou que uma regra acima mande avaliar, NÃO agende automaticamente — use a ferramenta encaminhar_humano para a secretária humana decidir e peça ao cliente para AGUARDAR. Nem sempre o certo é agendar.')
L.push('IMAGENS: se o cliente enviar uma imagem (foto de dente, exame), tente identificar do que se trata; se não estiver claro ou for caso clínico, trate como dúvida (encaminhar_humano).')
return L.length ? `\n\n${L.join('\n')}\n` : ''
}
// Formata o /status da agenda (funcionamento da clínica) para o system prompt.
// A Secretária usa isso para NÃO oferecer horários/agendar em dia/hora fechado.
static formatClinicStatus(st: any): string {