feat(secretaria): injeta funcionamento da clínica (horários/feriados) no prompt

brain.ts busca GET /status da agenda-bridge (aberto hoje?, próximo dia aberto,
grade da semana, feriados) e injeta no system prompt via formatClinicStatus,
junto da data já existente. Regra: a Secretária só oferece horário/agenda quando
a clínica está ABERTA — em dia fechado/feriado, informa o horário e oferece o
próximo dia disponível. (Par do satélite: scoreodonto #2.)

Testado em dev: ao pedirem "hoje" num domingo, ofereceu "amanhã" nos horários
configurados.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-06 01:24:20 +02:00
parent 9bbfe498e2
commit 983890c9be
3 changed files with 80 additions and 3 deletions
@@ -39,7 +39,7 @@ class ProtocolEngine {
if (!agent)
throw new Error('Agente não encontrado');
// Monta system prompt a partir dos nós ativos + contexto externo
const systemPrompt = await this.buildSystemPrompt(agent, conversation, opts, userMessage);
let systemPrompt = await this.buildSystemPrompt(agent, conversation, opts, userMessage);
// Carrega apenas as últimas N mensagens (não o histórico completo)
const contextWindow = agent.context_window ?? 8;
const recentMessages = await this.db('sec_messages')
@@ -114,6 +114,19 @@ class ProtocolEngine {
clinicaId: opts?.clinicaId ?? secCfg?.clinica_id,
},
};
// Funcionamento da clínica (aberto hoje?, feriados) — injeta no prompt para a
// Secretária saber quando dizer que está fechado, sem depender de tool call.
const statusClinicaId = opts?.clinicaId ?? secCfg?.clinica_id;
if (agendaUrl && statusClinicaId) {
try {
const u = new URL(`${agendaUrl.replace(/\/$/, '')}/status`);
u.searchParams.set('clinica_id', statusClinicaId);
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 */ }
}
let response;
let usageInfo = null;
let providerUsed = null;
@@ -173,6 +186,32 @@ class ProtocolEngine {
const p = (n, d = 2) => String(n).padStart(d, '0');
return `${p(now.getDate())}${p(now.getMonth() + 1)}${String(now.getFullYear()).slice(-2)}${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
}
// 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) {
if (!st?.hoje)
return '';
const dm = (s) => { const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/); return m ? `${m[3]}/${m[2]}` : s; };
const janelas = (arr) => (arr || []).map((h) => `${h.inicio}${h.fim}`).join(', ');
const L = ['=== FUNCIONAMENTO DA CLÍNICA ==='];
const h = st.hoje;
L.push(h.aberto
? `Hoje é ${h.dia_nome} (${dm(h.data)}): ABERTO — ${janelas(h.horarios)}.`
: `Hoje é ${h.dia_nome} (${dm(h.data)}): FECHADO${h.motivo ? ` (${h.motivo})` : ''}.`);
if (st.proximo_aberto && (!h.aberto || st.proximo_aberto.data !== h.data)) {
const p = st.proximo_aberto;
L.push(`Próximo dia aberto: ${p.dia_nome} (${dm(p.data)}), ${janelas(p.horarios)}.`);
}
if (Array.isArray(st.semana) && st.semana.length) {
const grade = st.semana.map((d) => `${d.dia_nome.slice(0, 3)} ${d.aberto ? janelas(d.horarios) : 'fechado'}`).join('; ');
L.push(`Grade da semana: ${grade}.`);
}
if (Array.isArray(st.feriados_proximos) && st.feriados_proximos.length) {
L.push(`Próximos feriados/fechamentos: ${st.feriados_proximos.map((f) => `${dm(f.data)} ${f.nome}`).join('; ')}.`);
}
L.push('IMPORTANTE: só ofereça horários e confirme agendamento quando a clínica estiver ABERTA. Se pedirem em dia/horário fechado ou feriado, informe o horário de funcionamento e ofereça o próximo dia disponível.');
return `\n\n${L.join('\n')}\n`;
}
// ── System Prompt Builder ─────────────────────────────────────────────────
async buildSystemPrompt(agent, conversation, opts, userMessage) {
const nodes = await this.db('sec_brain_nodes')