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:
@@ -39,7 +39,7 @@ class ProtocolEngine {
|
|||||||
if (!agent)
|
if (!agent)
|
||||||
throw new Error('Agente não encontrado');
|
throw new Error('Agente não encontrado');
|
||||||
// Monta system prompt a partir dos nós ativos + contexto externo
|
// 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)
|
// Carrega apenas as últimas N mensagens (não o histórico completo)
|
||||||
const contextWindow = agent.context_window ?? 8;
|
const contextWindow = agent.context_window ?? 8;
|
||||||
const recentMessages = await this.db('sec_messages')
|
const recentMessages = await this.db('sec_messages')
|
||||||
@@ -114,6 +114,19 @@ class ProtocolEngine {
|
|||||||
clinicaId: opts?.clinicaId ?? secCfg?.clinica_id,
|
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 response;
|
||||||
let usageInfo = null;
|
let usageInfo = null;
|
||||||
let providerUsed = null;
|
let providerUsed = null;
|
||||||
@@ -173,6 +186,32 @@ class ProtocolEngine {
|
|||||||
const p = (n, d = 2) => String(n).padStart(d, '0');
|
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())}`;
|
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 ─────────────────────────────────────────────────
|
// ── System Prompt Builder ─────────────────────────────────────────────────
|
||||||
async buildSystemPrompt(agent, conversation, opts, userMessage) {
|
async buildSystemPrompt(agent, conversation, opts, userMessage) {
|
||||||
const nodes = await this.db('sec_brain_nodes')
|
const nodes = await this.db('sec_brain_nodes')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -57,7 +57,7 @@ export class ProtocolEngine {
|
|||||||
if (!agent) throw new Error('Agente não encontrado')
|
if (!agent) throw new Error('Agente não encontrado')
|
||||||
|
|
||||||
// Monta system prompt a partir dos nós ativos + contexto externo
|
// 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)
|
// Carrega apenas as últimas N mensagens (não o histórico completo)
|
||||||
const contextWindow: number = agent.context_window ?? 8
|
const contextWindow: number = agent.context_window ?? 8
|
||||||
@@ -138,6 +138,18 @@ export class ProtocolEngine {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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: string
|
let response: string
|
||||||
let usageInfo: any = null
|
let usageInfo: any = null
|
||||||
let providerUsed: string | null = null
|
let providerUsed: string | null = null
|
||||||
@@ -203,6 +215,32 @@ 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())}`
|
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: any): string {
|
||||||
|
if (!st?.hoje) return ''
|
||||||
|
const dm = (s: string) => { const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/); return m ? `${m[3]}/${m[2]}` : s }
|
||||||
|
const janelas = (arr: any[]) => (arr || []).map((h) => `${h.inicio}–${h.fim}`).join(', ')
|
||||||
|
const L: string[] = ['=== 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: any) => `${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: any) => `${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 ─────────────────────────────────────────────────
|
// ── System Prompt Builder ─────────────────────────────────────────────────
|
||||||
|
|
||||||
private async buildSystemPrompt(
|
private async buildSystemPrompt(
|
||||||
|
|||||||
Reference in New Issue
Block a user