feat(secretaria): follow-up proativo com atraso (Parte B)

Quando o cliente insiste num horário mais tarde, a IA usa confirmar_depois:
diz "vou confirmar, aguarde uns minutinhos" e registra um follow-up
(sec_agenda_followups, fire_at = now + followup_delay_min, default 15). Um worker
no ext-api (a cada 30s) processa os vencidos: consulta /slots perto do horário
desejado e manda ao paciente "Oie, consegui! Posso às X?" (assinado pelo agente).

Testado em dev: worker processa follow-up vencido, consulta a agenda e envia.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-06 06:15:26 +02:00
parent 163a5d0db2
commit 02a952c60b
12 changed files with 226 additions and 4 deletions
@@ -390,6 +390,48 @@ exports.BUILTIN_TOOLS = [
}
},
},
// ── confirmar_depois ────────────────────────────────────────────────────────
// Parte B: o cliente pede um horário MAIS TARDE que o oferecido. A IA diz "vou
// confirmar, aguarde uns minutinhos" e registra um follow-up; um worker retorna
// depois (com atraso) com o horário confirmado.
{
name: 'confirmar_depois',
description: 'Use quando o cliente pedir um horário MAIS TARDE no dia (dentro do mesmo período) do que o que você ' +
'ofereceu — ou quando quiser "checar a agenda com calma" antes de confirmar. Diga ao cliente para aguardar ' +
'uns minutinhos que você vai confirmar; em poucos minutos o sistema retorna sozinho com o horário. NÃO ofereça horário agora.',
parameters: {
type: 'object',
properties: {
data: { type: 'string', description: 'Data desejada YYYY-MM-DD.' },
periodo: { type: 'string', description: '"manha" ou "tarde".', enum: ['manha', 'tarde'] },
hora_desejada: { type: 'string', description: 'Horário aproximado que o cliente pediu (HH:MM), se houver.' },
nome_cliente: { type: 'string', description: 'Nome do cliente.' },
dentista: { type: 'string', description: 'Dentista, se indicado (opcional).' },
},
required: ['data'],
},
async execute(args, ctx) {
if (!ctx.agenda?.url || !ctx.agenda.clinicaId)
return { ok: false, erro: 'Agenda indisponível.' };
const jid = (ctx.extChatId ?? '').split(':').pop() ?? '';
if (!jid || !ctx.instanceId)
return { ok: false, erro: 'Contexto incompleto.' };
const delay = Math.max(1, ctx.followupDelayMin ?? 15);
try {
await ctx.db('sec_agenda_followups').insert({
tenant_id: ctx.tenantId ?? null, instance_id: ctx.instanceId, jid,
clinica_id: ctx.agenda.clinicaId, agenda_url: ctx.agenda.url, agenda_secret: ctx.agenda.secret ?? '',
dentista_nome: args['dentista'] ?? null, data: args['data'], hora_desejada: args['hora_desejada'] ?? null,
periodo: args['periodo'] ?? null, paciente_nome: args['nome_cliente'] ?? null,
fire_at: new Date(Date.now() + delay * 60000), status: 'pendente',
});
return { ok: true, agendado_retorno: true, instrucao: 'Diga ao cliente para aguardar uns minutinhos que você vai confirmar a agenda e JÁ retorna. NÃO ofereça horário agora — o sistema retorna sozinho em instantes.' };
}
catch (e) {
return { ok: false, erro: e.message };
}
},
},
];
function resolveTools(names) {
return names