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:
@@ -354,6 +354,53 @@ function buildExtRoutes(prisma, manager, db, config, hooks, io) {
|
|||||||
const secAgent = await db('sec_agents').where({ id: conv.agent_id }).select('name').first();
|
const secAgent = await db('sec_agents').where({ id: conv.agent_id }).select('name').first();
|
||||||
await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply, agentName: secAgent?.name });
|
await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply, agentName: secAgent?.name });
|
||||||
});
|
});
|
||||||
|
// ── Worker: follow-ups proativos da agenda (Parte B) ──────────────────────
|
||||||
|
// A cada 30s processa os follow-ups vencidos (confirmar_depois): consulta a
|
||||||
|
// agenda perto do horário desejado e manda ao paciente "Oie, consegui!".
|
||||||
|
setInterval(async () => {
|
||||||
|
let due = [];
|
||||||
|
try {
|
||||||
|
due = await db('sec_agenda_followups').where('status', 'pendente').where('fire_at', '<=', new Date()).limit(10);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const fu of due) {
|
||||||
|
try {
|
||||||
|
const claimed = await db('sec_agenda_followups').where({ id: fu.id, status: 'pendente' }).update({ status: 'processando' });
|
||||||
|
if (!claimed)
|
||||||
|
continue;
|
||||||
|
const dataStr = fu.data ? (fu.data instanceof Date ? fu.data.toISOString().slice(0, 10) : String(fu.data).slice(0, 10)) : null;
|
||||||
|
const u = new URL(`${String(fu.agenda_url).replace(/\/$/, '')}/slots`);
|
||||||
|
u.searchParams.set('clinica_id', fu.clinica_id);
|
||||||
|
if (dataStr)
|
||||||
|
u.searchParams.set('date', dataStr);
|
||||||
|
if (fu.periodo)
|
||||||
|
u.searchParams.set('periodo', fu.periodo);
|
||||||
|
const r = await fetch(u, { headers: { 'x-nw-agenda-secret': fu.agenda_secret || '' }, signal: AbortSignal.timeout(12000) });
|
||||||
|
const j = r.ok ? await r.json() : {};
|
||||||
|
const slots = j?.slots ?? [];
|
||||||
|
// slot mais próximo do horário desejado (senão o primeiro).
|
||||||
|
let escolhido = slots[0];
|
||||||
|
if (fu.hora_desejada && slots.length) {
|
||||||
|
const alvo = String(fu.hora_desejada).slice(0, 5);
|
||||||
|
escolhido = slots.slice().sort((a, b) => Math.abs(a.start.slice(11, 16).localeCompare(alvo)) - Math.abs(b.start.slice(11, 16).localeCompare(alvo)))[0] || slots[0];
|
||||||
|
}
|
||||||
|
const texto = escolhido
|
||||||
|
? `Oie! Consegui aqui 😊 Posso te agendar${dataStr ? ` dia ${dataStr.slice(8, 10)}/${dataStr.slice(5, 7)}` : ''} às ${escolhido.start.slice(11, 16)}. Fica bom pra você?`
|
||||||
|
: 'Oi! Verifiquei a agenda e não consegui esse horário 😕 Posso te oferecer outro dia ou período?';
|
||||||
|
// assina com o agente do número (channel) para manter o padrão.
|
||||||
|
const ch = await db('sec_numbers').where({ instance_id: fu.instance_id }).first();
|
||||||
|
const ag = ch?.agent_id ? await db('sec_agents').where({ id: ch.agent_id }).first() : await db('sec_agents').where({ active: true }).orderBy('created_at').first();
|
||||||
|
const chat = await prisma.chat.findFirst({ where: { instanceId: fu.instance_id, jid: fu.jid }, orderBy: { updatedAt: 'desc' }, select: { id: true } });
|
||||||
|
await sendSecretariaReply({ instanceId: fu.instance_id, tenantId: fu.tenant_id, chatId: chat?.id ?? fu.jid, jid: fu.jid, reply: texto, agentName: ag?.name });
|
||||||
|
await db('sec_agenda_followups').where({ id: fu.id }).update({ status: 'enviado' });
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
await db('sec_agenda_followups').where({ id: fu.id }).update({ status: 'falhou' }).catch(() => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
// ── Escalation notification listener ──────────────────────────────────────
|
// ── Escalation notification listener ──────────────────────────────────────
|
||||||
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
||||||
hooks.register('ext:escalated', async (data) => {
|
hooks.register('ext:escalated', async (data) => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -339,6 +339,47 @@ export function buildExtRoutes(
|
|||||||
await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply, agentName: secAgent?.name })
|
await sendSecretariaReply({ instanceId, tenantId, chatId, jid, reply, agentName: secAgent?.name })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Worker: follow-ups proativos da agenda (Parte B) ──────────────────────
|
||||||
|
// A cada 30s processa os follow-ups vencidos (confirmar_depois): consulta a
|
||||||
|
// agenda perto do horário desejado e manda ao paciente "Oie, consegui!".
|
||||||
|
setInterval(async () => {
|
||||||
|
let due: any[] = []
|
||||||
|
try { due = await db('sec_agenda_followups').where('status', 'pendente').where('fire_at', '<=', new Date()).limit(10) }
|
||||||
|
catch { return }
|
||||||
|
for (const fu of due) {
|
||||||
|
try {
|
||||||
|
const claimed = await db('sec_agenda_followups').where({ id: fu.id, status: 'pendente' }).update({ status: 'processando' })
|
||||||
|
if (!claimed) continue
|
||||||
|
const dataStr = fu.data ? (fu.data instanceof Date ? fu.data.toISOString().slice(0, 10) : String(fu.data).slice(0, 10)) : null
|
||||||
|
const u = new URL(`${String(fu.agenda_url).replace(/\/$/, '')}/slots`)
|
||||||
|
u.searchParams.set('clinica_id', fu.clinica_id)
|
||||||
|
if (dataStr) u.searchParams.set('date', dataStr)
|
||||||
|
if (fu.periodo) u.searchParams.set('periodo', fu.periodo)
|
||||||
|
const r = await fetch(u, { headers: { 'x-nw-agenda-secret': fu.agenda_secret || '' }, signal: AbortSignal.timeout(12_000) })
|
||||||
|
const j: any = r.ok ? await r.json() : {}
|
||||||
|
const slots: any[] = j?.slots ?? []
|
||||||
|
// slot mais próximo do horário desejado (senão o primeiro).
|
||||||
|
let escolhido = slots[0]
|
||||||
|
if (fu.hora_desejada && slots.length) {
|
||||||
|
const alvo = String(fu.hora_desejada).slice(0, 5)
|
||||||
|
escolhido = slots.slice().sort((a, b) =>
|
||||||
|
Math.abs(a.start.slice(11, 16).localeCompare(alvo)) - Math.abs(b.start.slice(11, 16).localeCompare(alvo)))[0] || slots[0]
|
||||||
|
}
|
||||||
|
const texto = escolhido
|
||||||
|
? `Oie! Consegui aqui 😊 Posso te agendar${dataStr ? ` dia ${dataStr.slice(8, 10)}/${dataStr.slice(5, 7)}` : ''} às ${escolhido.start.slice(11, 16)}. Fica bom pra você?`
|
||||||
|
: 'Oi! Verifiquei a agenda e não consegui esse horário 😕 Posso te oferecer outro dia ou período?'
|
||||||
|
// assina com o agente do número (channel) para manter o padrão.
|
||||||
|
const ch = await db('sec_numbers').where({ instance_id: fu.instance_id }).first()
|
||||||
|
const ag = ch?.agent_id ? await db('sec_agents').where({ id: ch.agent_id }).first() : await db('sec_agents').where({ active: true }).orderBy('created_at').first()
|
||||||
|
const chat = await prisma.chat.findFirst({ where: { instanceId: fu.instance_id, jid: fu.jid }, orderBy: { updatedAt: 'desc' }, select: { id: true } })
|
||||||
|
await sendSecretariaReply({ instanceId: fu.instance_id, tenantId: fu.tenant_id, chatId: chat?.id ?? fu.jid, jid: fu.jid, reply: texto, agentName: ag?.name })
|
||||||
|
await db('sec_agenda_followups').where({ id: fu.id }).update({ status: 'enviado' })
|
||||||
|
} catch {
|
||||||
|
await db('sec_agenda_followups').where({ id: fu.id }).update({ status: 'falhou' }).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30_000)
|
||||||
|
|
||||||
// ── Escalation notification listener ──────────────────────────────────────
|
// ── Escalation notification listener ──────────────────────────────────────
|
||||||
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
// When escalar_humano tool fires, send a WA message to the configured admin phone.
|
||||||
hooks.register('ext:escalated', async (data: any) => {
|
hooks.register('ext:escalated', async (data: any) => {
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class ProtocolEngine {
|
|||||||
extChatId: conversation.ext_chat_id ?? undefined,
|
extChatId: conversation.ext_chat_id ?? undefined,
|
||||||
tenantId: opts?.tenantId,
|
tenantId: opts?.tenantId,
|
||||||
instanceId: opts?.instanceId,
|
instanceId: opts?.instanceId,
|
||||||
|
followupDelayMin: Number(secCfg?.followup_delay_min) || 15,
|
||||||
hooks: opts?.hooks,
|
hooks: opts?.hooks,
|
||||||
agenda: {
|
agenda: {
|
||||||
url: agendaUrl,
|
url: agendaUrl,
|
||||||
@@ -214,6 +215,7 @@ class ProtocolEngine {
|
|||||||
L.push('1) NUNCA liste vários horários de uma vez. PRIMEIRO pergunte: "Você prefere de manhã ou à tarde?".');
|
L.push('1) NUNCA liste vários horários de uma vez. PRIMEIRO pergunte: "Você prefere de manhã ou à tarde?".');
|
||||||
L.push('2) Com a resposta, chame listar_horarios passando periodo=manha ou tarde e ofereça UM ÚNICO horário — sempre o PRIMEIRO livre daquele período. Ex.: "Consigo te encaixar às 10:00, fica bom?".');
|
L.push('2) Com a resposta, chame listar_horarios passando periodo=manha ou tarde e ofereça UM ÚNICO horário — sempre o PRIMEIRO livre daquele período. Ex.: "Consigo te encaixar às 10:00, fica bom?".');
|
||||||
L.push('3) Se não servir, ofereça o PRÓXIMO horário livre, um de cada vez (nunca uma lista). Mantenha sempre 1 hora entre os atendimentos.');
|
L.push('3) Se não servir, ofereça o PRÓXIMO horário livre, um de cada vez (nunca uma lista). Mantenha sempre 1 hora entre os atendimentos.');
|
||||||
|
L.push('3b) Se o cliente insistir num horário MAIS TARDE no dia, use a ferramenta confirmar_depois: diga que vai confirmar a agenda e para aguardar uns minutinhos — o sistema retorna sozinho com o horário. NÃO ofereça o horário na hora.');
|
||||||
L.push('4) 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 nem confirme: use a ferramenta solicitar_horario_especial e responda algo como "No momento esse horário não está disponível, mas ainda não confirmei a agenda — posso te confirmar daqui a pouco?" (sem mencionar "secretária").');
|
L.push('4) 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 nem confirme: use a ferramenta solicitar_horario_especial e responda algo como "No momento esse horário não está disponível, mas ainda não confirmei a agenda — posso te confirmar daqui a pouco?" (sem mencionar "secretária").');
|
||||||
return `\n\n${L.join('\n')}\n`;
|
return `\n\n${L.join('\n')}\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -129,6 +129,7 @@ export class ProtocolEngine {
|
|||||||
extChatId: conversation.ext_chat_id ?? undefined,
|
extChatId: conversation.ext_chat_id ?? undefined,
|
||||||
tenantId: opts?.tenantId,
|
tenantId: opts?.tenantId,
|
||||||
instanceId: opts?.instanceId,
|
instanceId: opts?.instanceId,
|
||||||
|
followupDelayMin: Number(secCfg?.followup_delay_min) || 15,
|
||||||
hooks: opts?.hooks,
|
hooks: opts?.hooks,
|
||||||
agenda: {
|
agenda: {
|
||||||
url: agendaUrl,
|
url: agendaUrl,
|
||||||
@@ -242,6 +243,7 @@ export class ProtocolEngine {
|
|||||||
L.push('1) NUNCA liste vários horários de uma vez. PRIMEIRO pergunte: "Você prefere de manhã ou à tarde?".')
|
L.push('1) NUNCA liste vários horários de uma vez. PRIMEIRO pergunte: "Você prefere de manhã ou à tarde?".')
|
||||||
L.push('2) Com a resposta, chame listar_horarios passando periodo=manha ou tarde e ofereça UM ÚNICO horário — sempre o PRIMEIRO livre daquele período. Ex.: "Consigo te encaixar às 10:00, fica bom?".')
|
L.push('2) Com a resposta, chame listar_horarios passando periodo=manha ou tarde e ofereça UM ÚNICO horário — sempre o PRIMEIRO livre daquele período. Ex.: "Consigo te encaixar às 10:00, fica bom?".')
|
||||||
L.push('3) Se não servir, ofereça o PRÓXIMO horário livre, um de cada vez (nunca uma lista). Mantenha sempre 1 hora entre os atendimentos.')
|
L.push('3) Se não servir, ofereça o PRÓXIMO horário livre, um de cada vez (nunca uma lista). Mantenha sempre 1 hora entre os atendimentos.')
|
||||||
|
L.push('3b) Se o cliente insistir num horário MAIS TARDE no dia, use a ferramenta confirmar_depois: diga que vai confirmar a agenda e para aguardar uns minutinhos — o sistema retorna sozinho com o horário. NÃO ofereça o horário na hora.')
|
||||||
L.push('4) 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 nem confirme: use a ferramenta solicitar_horario_especial e responda algo como "No momento esse horário não está disponível, mas ainda não confirmei a agenda — posso te confirmar daqui a pouco?" (sem mencionar "secretária").')
|
L.push('4) 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 nem confirme: use a ferramenta solicitar_horario_especial e responda algo como "No momento esse horário não está disponível, mas ainda não confirmei a agenda — posso te confirmar daqui a pouco?" (sem mencionar "secretária").')
|
||||||
return `\n\n${L.join('\n')}\n`
|
return `\n\n${L.join('\n')}\n`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,29 @@ async function runMigrations(db) {
|
|||||||
});
|
});
|
||||||
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_mem_contact ON sec_contact_memory (agent_id, contact_key)');
|
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_mem_contact ON sec_contact_memory (agent_id, contact_key)');
|
||||||
}
|
}
|
||||||
|
// ── sec_agenda_followups — follow-up proativo (Parte B): a IA promete "confirmo
|
||||||
|
// em uns minutos" e um worker envia depois o horário confirmado, com atraso.
|
||||||
|
if (!(await db.schema.hasTable('sec_agenda_followups'))) {
|
||||||
|
await db.schema.createTable('sec_agenda_followups', (t) => {
|
||||||
|
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'));
|
||||||
|
t.string('tenant_id', 80);
|
||||||
|
t.string('instance_id', 100);
|
||||||
|
t.string('chat_id', 300);
|
||||||
|
t.string('jid', 120); // paciente
|
||||||
|
t.string('clinica_id', 100);
|
||||||
|
t.string('agenda_url', 300);
|
||||||
|
t.string('agenda_secret', 200);
|
||||||
|
t.string('dentista_nome', 120);
|
||||||
|
t.date('data');
|
||||||
|
t.string('hora_desejada', 8);
|
||||||
|
t.string('periodo', 10); // manha | tarde
|
||||||
|
t.string('paciente_nome', 120);
|
||||||
|
t.timestamp('fire_at').notNullable();
|
||||||
|
t.string('status', 20).defaultTo('pendente'); // pendente | enviado | falhou
|
||||||
|
t.timestamps(true, true);
|
||||||
|
});
|
||||||
|
await db.raw('CREATE INDEX IF NOT EXISTS idx_followups_fire ON sec_agenda_followups (status, fire_at)');
|
||||||
|
}
|
||||||
// ── Seeds: agente padrão + nós + calendário ───────────────────────────────
|
// ── Seeds: agente padrão + nós + calendário ───────────────────────────────
|
||||||
const agentCount = await db('sec_agents').count('id as c').first();
|
const agentCount = await db('sec_agents').count('id as c').first();
|
||||||
if (Number(agentCount?.c ?? 0) === 0) {
|
if (Number(agentCount?.c ?? 0) === 0) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -208,6 +208,30 @@ export async function runMigrations(db: Knex): Promise<void> {
|
|||||||
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_mem_contact ON sec_contact_memory (agent_id, contact_key)')
|
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_mem_contact ON sec_contact_memory (agent_id, contact_key)')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── sec_agenda_followups — follow-up proativo (Parte B): a IA promete "confirmo
|
||||||
|
// em uns minutos" e um worker envia depois o horário confirmado, com atraso.
|
||||||
|
if (!(await db.schema.hasTable('sec_agenda_followups'))) {
|
||||||
|
await db.schema.createTable('sec_agenda_followups', (t) => {
|
||||||
|
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
|
||||||
|
t.string('tenant_id', 80)
|
||||||
|
t.string('instance_id', 100)
|
||||||
|
t.string('chat_id', 300)
|
||||||
|
t.string('jid', 120) // paciente
|
||||||
|
t.string('clinica_id', 100)
|
||||||
|
t.string('agenda_url', 300)
|
||||||
|
t.string('agenda_secret', 200)
|
||||||
|
t.string('dentista_nome', 120)
|
||||||
|
t.date('data')
|
||||||
|
t.string('hora_desejada', 8)
|
||||||
|
t.string('periodo', 10) // manha | tarde
|
||||||
|
t.string('paciente_nome', 120)
|
||||||
|
t.timestamp('fire_at').notNullable()
|
||||||
|
t.string('status', 20).defaultTo('pendente') // pendente | enviado | falhou
|
||||||
|
t.timestamps(true, true)
|
||||||
|
})
|
||||||
|
await db.raw('CREATE INDEX IF NOT EXISTS idx_followups_fire ON sec_agenda_followups (status, fire_at)')
|
||||||
|
}
|
||||||
|
|
||||||
// ── Seeds: agente padrão + nós + calendário ───────────────────────────────
|
// ── Seeds: agente padrão + nós + calendário ───────────────────────────────
|
||||||
const agentCount = await db('sec_agents').count('id as c').first()
|
const agentCount = await db('sec_agents').count('id as c').first()
|
||||||
if (Number(agentCount?.c ?? 0) === 0) {
|
if (Number(agentCount?.c ?? 0) === 0) {
|
||||||
|
|||||||
@@ -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) {
|
function resolveTools(names) {
|
||||||
return names
|
return names
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -39,6 +39,8 @@ export interface ToolContext {
|
|||||||
tenantId?: string
|
tenantId?: string
|
||||||
/** Número/sessão que recebeu a mensagem — escopa o calendário interno por sessão. */
|
/** Número/sessão que recebeu a mensagem — escopa o calendário interno por sessão. */
|
||||||
instanceId?: string
|
instanceId?: string
|
||||||
|
/** Atraso (min) do follow-up proativo da agenda (Parte B). Default 15. */
|
||||||
|
followupDelayMin?: number
|
||||||
hooks?: HookBus
|
hooks?: HookBus
|
||||||
/** Ponte de agenda do satélite (scoreodonto). Quando presente, as tools de
|
/** Ponte de agenda do satélite (scoreodonto). Quando presente, as tools de
|
||||||
* agenda operam na agenda REAL da clínica em vez do calendário interno. */
|
* agenda operam na agenda REAL da clínica em vez do calendário interno. */
|
||||||
@@ -429,6 +431,45 @@ export const BUILTIN_TOOLS: ToolDef[] = [
|
|||||||
} catch (e: any) { return { ok: false, erro: e.message } }
|
} catch (e: any) { return { ok: false, erro: e.message } }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── 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 * 60_000), 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: any) { return { ok: false, erro: e.message } }
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function resolveTools(names: string[]): ToolDef[] {
|
export function resolveTools(names: string[]): ToolDef[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user