feat(secretaria): tool solicitar_horario_especial (zona cinza)

Quando o cliente pede um horário PONTUAL fora dos disponíveis (mais cedo/mais
tarde), a IA usa solicitar_horario_especial → registra um pedido na agenda do
satélite (POST /pedido) e responde "vou confirmar com a secretária e retorno"
(sem garantir o horário). brain.ts injeta essa regra no prompt. Par do satélite
(scoreodonto): janela flex + fila da secretária humana.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-06 05:29:45 +02:00
parent 837a7a44d9
commit 0f20fe5314
6 changed files with 100 additions and 2 deletions
@@ -210,6 +210,7 @@ class ProtocolEngine {
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.');
L.push('Se o cliente pedir um horário PONTUAL fora dos disponíveis (mais cedo/mais tarde, ex.: 8h30 ou depois das 17h), NÃO recuse: use a ferramenta solicitar_horario_especial e diga que vai confirmar com a secretária e retornar.');
return `\n\n${L.join('\n')}\n`;
}
// ── System Prompt Builder ─────────────────────────────────────────────────
File diff suppressed because one or more lines are too long
@@ -238,6 +238,7 @@ export class ProtocolEngine {
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.')
L.push('Se o cliente pedir um horário PONTUAL fora dos disponíveis (mais cedo/mais tarde, ex.: 8h30 ou depois das 17h), NÃO recuse: use a ferramenta solicitar_horario_especial e diga que vai confirmar com a secretária e retornar.')
return `\n\n${L.join('\n')}\n`
}
@@ -331,6 +331,57 @@ exports.BUILTIN_TOOLS = [
return { ok: true, protocolo: conv?.protocol_number ?? '', resumo: summary };
},
},
// ── solicitar_horario_especial ──────────────────────────────────────────────
// Zona cinza: o cliente pede um horário FORA da janela livre (mais cedo/mais
// tarde). A IA NÃO agenda — registra um pedido para a secretária HUMANA confirmar
// e diz que vai retornar.
{
name: 'solicitar_horario_especial',
description: 'Use QUANDO o cliente pedir um horário que NÃO aparece na lista de horários disponíveis — ' +
'mais cedo ou mais tarde que o normal (ex.: "tem às 8h30?", "consegue depois das 17h?"). ' +
'NUNCA diga que não tem: registre o pedido com esta tool e diga ao cliente que você vai ' +
'CONFIRMAR com a secretária e RETORNAR. Não garanta o horário. Só para horários pontuais fora do padrão.',
parameters: {
type: 'object',
properties: {
data: { type: 'string', description: 'Data desejada no formato YYYY-MM-DD.' },
hora: { type: 'string', description: 'Horário desejado HH:MM.' },
nome_cliente: { type: 'string', description: 'Nome completo do cliente.' },
telefone_cliente: { type: 'string', description: 'Telefone do cliente (opcional).' },
dentista: { type: 'string', description: 'Nome do dentista, se o cliente indicou (opcional).' },
procedimento: { type: 'string', description: 'Procedimento/motivo (opcional).' },
},
required: ['data', 'hora', 'nome_cliente'],
},
async execute(args, ctx) {
if (!ctx.agenda?.url || !ctx.agenda.clinicaId)
return { ok: false, erro: 'Agenda indisponível.' };
let telefone = String(args['telefone_cliente'] ?? '').replace(/\D/g, '');
if (!telefone && ctx.extChatId) {
const jid = ctx.extChatId.split(':').pop() ?? '';
telefone = (jid.split('@')[0] ?? '').replace(/\D/g, '');
}
try {
const r = await fetch(`${ctx.agenda.url.replace(/\/$/, '')}/pedido`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-nw-agenda-secret': ctx.agenda.secret ?? '' },
signal: AbortSignal.timeout(12000),
body: JSON.stringify({
clinica_id: ctx.agenda.clinicaId, data: args['data'], hora: args['hora'],
dentista_nome: args['dentista'] ?? null, nome_cliente: args['nome_cliente'],
telefone, procedimento: args['procedimento'] ?? null,
}),
});
const j = await r.json().catch(() => ({}));
if (!r.ok || j.ok === false)
return { ok: false, mensagem: j.motivo ?? 'Não foi possível registrar o pedido.' };
return { ok: true, registrado: true, instrucao: 'Diga ao cliente que você vai VERIFICAR esse horário com a secretária e RETORNA com a confirmação. NÃO garanta o horário agora.' };
}
catch (e) {
return { ok: false, erro: e.message };
}
},
},
];
function resolveTools(names) {
return names
File diff suppressed because one or more lines are too long
@@ -377,6 +377,51 @@ export const BUILTIN_TOOLS: ToolDef[] = [
return { ok: true, protocolo: conv?.protocol_number ?? '', resumo: summary }
},
},
// ── solicitar_horario_especial ──────────────────────────────────────────────
// Zona cinza: o cliente pede um horário FORA da janela livre (mais cedo/mais
// tarde). A IA NÃO agenda — registra um pedido para a secretária HUMANA confirmar
// e diz que vai retornar.
{
name: 'solicitar_horario_especial',
description:
'Use QUANDO o cliente pedir um horário que NÃO aparece na lista de horários disponíveis — ' +
'mais cedo ou mais tarde que o normal (ex.: "tem às 8h30?", "consegue depois das 17h?"). ' +
'NUNCA diga que não tem: registre o pedido com esta tool e diga ao cliente que você vai ' +
'CONFIRMAR com a secretária e RETORNAR. Não garanta o horário. Só para horários pontuais fora do padrão.',
parameters: {
type: 'object',
properties: {
data: { type: 'string', description: 'Data desejada no formato YYYY-MM-DD.' },
hora: { type: 'string', description: 'Horário desejado HH:MM.' },
nome_cliente: { type: 'string', description: 'Nome completo do cliente.' },
telefone_cliente: { type: 'string', description: 'Telefone do cliente (opcional).' },
dentista: { type: 'string', description: 'Nome do dentista, se o cliente indicou (opcional).' },
procedimento: { type: 'string', description: 'Procedimento/motivo (opcional).' },
},
required: ['data', 'hora', 'nome_cliente'],
},
async execute(args, ctx) {
if (!ctx.agenda?.url || !ctx.agenda.clinicaId) return { ok: false, erro: 'Agenda indisponível.' }
let telefone = String(args['telefone_cliente'] ?? '').replace(/\D/g, '')
if (!telefone && ctx.extChatId) { const jid = ctx.extChatId.split(':').pop() ?? ''; telefone = (jid.split('@')[0] ?? '').replace(/\D/g, '') }
try {
const r = await fetch(`${ctx.agenda.url.replace(/\/$/, '')}/pedido`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-nw-agenda-secret': ctx.agenda.secret ?? '' },
signal: AbortSignal.timeout(12_000),
body: JSON.stringify({
clinica_id: ctx.agenda.clinicaId, data: args['data'], hora: args['hora'],
dentista_nome: args['dentista'] ?? null, nome_cliente: args['nome_cliente'],
telefone, procedimento: args['procedimento'] ?? null,
}),
})
const j: any = await r.json().catch(() => ({}))
if (!r.ok || j.ok === false) return { ok: false, mensagem: j.motivo ?? 'Não foi possível registrar o pedido.' }
return { ok: true, registrado: true, instrucao: 'Diga ao cliente que você vai VERIFICAR esse horário com a secretária e RETORNA com a confirmação. NÃO garanta o horário agora.' }
} catch (e: any) { return { ok: false, erro: e.message } }
},
},
]
export function resolveTools(names: string[]): ToolDef[] {