"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ALL_TOOL_NAMES = exports.BUILTIN_TOOLS = void 0; exports.resolveTools = resolveTools; // ── Helpers ──────────────────────────────────────────────────────────────────── function fmtDate(d) { return d.toISOString().split('T')[0]; } // ── Tools ────────────────────────────────────────────────────────────────────── exports.BUILTIN_TOOLS = [ // ── listar_horarios ────────────────────────────────────────────────────────── { name: 'listar_horarios', description: 'Lista os horários disponíveis na agenda para agendamento. ' + 'Use antes de propor datas ao cliente. ' + 'Se não informar a data, retorna os próximos 7 dias.', parameters: { type: 'object', properties: { data: { type: 'string', description: 'Data no formato YYYY-MM-DD. Opcional — padrão: próximos 7 dias.', }, }, }, async execute(args, ctx) { // Agenda REAL do scoreodonto (ponte) quando configurada. if (ctx.agenda?.url && ctx.agenda.clinicaId) { try { const u = new URL(`${ctx.agenda.url.replace(/\/$/, '')}/slots`); u.searchParams.set('clinica_id', ctx.agenda.clinicaId); if (args['data']) u.searchParams.set('date', String(args['data'])); const r = await fetch(u, { headers: { 'x-nw-agenda-secret': ctx.agenda.secret ?? '' }, signal: AbortSignal.timeout(12000), }); const j = await r.json().catch(() => ({})); const slots = j?.slots ?? []; if (!slots.length) return { disponivel: false, mensagem: j?.motivo ?? 'Nenhum horário disponível no período.' }; return { disponivel: true, horarios: slots.map((s) => ({ // slot_id sintético (não há registro persistente — o slot é calculado) id: `${s.dentista_id}|${s.start}|${s.end}`, dentista: s.dentista_nome, data: String(s.start).slice(0, 10), inicio: String(s.start).slice(11, 16), fim: String(s.end).slice(11, 16), })), }; } catch (e) { return { disponivel: false, mensagem: `Agenda indisponível: ${e.message}` }; } } // Fallback: calendário interno do motor. const from = args['data'] ?? fmtDate(new Date()); const to = args['data'] ?? fmtDate(new Date(Date.now() + 7 * 86400000)); const slots = await ctx.db('sec_calendar') .where('status', 'available') .whereBetween('date', [from, to]) .orderBy('date').orderBy('time_start') .limit(20); if (!slots.length) { return { disponivel: false, mensagem: 'Nenhum horário disponível no período informado.' }; } return { disponivel: true, horarios: slots.map(s => ({ id: s.id, data: s.date, inicio: String(s.time_start).slice(0, 5), fim: String(s.time_end).slice(0, 5), titulo: s.title, })), }; }, }, // ── agendar_horario ────────────────────────────────────────────────────────── { name: 'agendar_horario', description: 'Reserva um horário disponível na agenda para o cliente. ' + 'Use listar_horarios primeiro para obter o slot_id correto.', parameters: { type: 'object', properties: { slot_id: { type: 'string', description: 'ID do horário retornado por listar_horarios.' }, nome_cliente: { type: 'string', description: 'Nome completo do cliente.' }, telefone_cliente: { type: 'string', description: 'Telefone do cliente (opcional).' }, }, required: ['slot_id', 'nome_cliente'], }, async execute(args, ctx) { // Agenda REAL do scoreodonto (ponte) quando configurada. if (ctx.agenda?.url && ctx.agenda.clinicaId) { const parts = String(args['slot_id'] ?? '').split('|'); if (parts.length !== 3) return { ok: false, erro: 'slot_id inválido. Chame listar_horarios novamente.' }; const [dentistaId, start, end] = parts; // telefone: do argumento OU derivado do JID do contato (extChatId = tenant:jid) let telefone = String(args['telefone_cliente'] ?? '').replace(/\D/g, ''); if (!telefone && ctx.extChatId) { const jid = ctx.extChatId.split(':').slice(1).join(':'); telefone = (jid.split('@')[0] ?? '').replace(/\D/g, ''); } try { const r = await fetch(`${ctx.agenda.url.replace(/\/$/, '')}/book`, { 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, dentista_id: dentistaId, start, end, paciente_nome: args['nome_cliente'], paciente_celular: telefone || null, }), }); const j = await r.json().catch(() => ({})); if (r.status === 409) return { ok: false, erro: 'Esse horário acabou de ser ocupado. Chame listar_horarios novamente.' }; if (!r.ok) return { ok: false, erro: j?.error ?? 'Falha ao agendar.' }; return { ok: true, confirmacao: { data: String(start).slice(0, 10), inicio: String(start).slice(11, 16), fim: String(end).slice(11, 16), nome: args['nome_cliente'], paciente_vinculado: !!j?.paciente_vinculado, }, }; } catch (e) { return { ok: false, erro: `Agenda indisponível: ${e.message}` }; } } // Fallback: calendário interno do motor. const slot = await ctx.db('sec_calendar') .where({ id: args['slot_id'], status: 'available' }) .first(); if (!slot) { return { ok: false, erro: 'Horário não encontrado ou já reservado. Chame listar_horarios novamente.' }; } await ctx.db('sec_calendar').where({ id: args['slot_id'] }).update({ status: 'booked', attendee_name: args['nome_cliente'], attendee_phone: args['telefone_cliente'] ?? null, updated_at: new Date(), }); return { ok: true, confirmacao: { data: slot.date, inicio: String(slot.time_start).slice(0, 5), fim: String(slot.time_end).slice(0, 5), titulo: slot.title, nome: args['nome_cliente'], }, }; }, }, // ── escalar_humano ─────────────────────────────────────────────────────────── { name: 'escalar_humano', description: 'Transfere o atendimento para um atendente humano quando a situação exige. ' + 'Use quando: cliente pede explicitamente, problema financeiro sensível, raiva intensa, ' + 'ou quando você não consegue resolver após tentativas honestas.', parameters: { type: 'object', properties: { motivo: { type: 'string', description: 'Motivo da transferência (ex: "cliente insatisfeito com cobrança").', }, }, }, async execute(args, ctx) { await ctx.db('sec_conversations').where({ id: ctx.conversationId }).update({ handoff_mode: 'humano', handoff_human_at: new Date(), status: 'escalated', }); const conv = await ctx.db('sec_conversations').where({ id: ctx.conversationId }).first(); if (ctx.hooks && ctx.tenantId && ctx.extChatId) { const chatId = ctx.extChatId.includes(':') ? ctx.extChatId.split(':').slice(1).join(':') : ctx.extChatId; const payload = { tenantId: ctx.tenantId, conversationId: ctx.conversationId, chatId, protocolNumber: conv?.protocol_number ?? '', motivo: args['motivo'] ?? '', }; ctx.hooks.emit('ext:handoff', { ...payload, mode: 'humano', reason: 'escalation' }).catch(() => { }); ctx.hooks.emit('ext:escalated', payload).catch(() => { }); } return { ok: true, escalado: true, motivo: args['motivo'] ?? 'Solicitado pelo sistema' }; }, }, // ── encerrar_protocolo ─────────────────────────────────────────────────────── { name: 'encerrar_protocolo', description: 'Encerra o protocolo de atendimento após resolver o problema do cliente. ' + 'Use apenas quando tiver certeza de que tudo foi resolvido.', parameters: { type: 'object', properties: { resumo: { type: 'string', description: 'Breve resumo do que foi tratado e resolvido neste atendimento.', }, }, required: ['resumo'], }, async execute(args, ctx) { const summary = args['resumo'] ?? 'Atendimento encerrado.'; const conv = await ctx.db('sec_conversations').where({ id: ctx.conversationId }).first(); await ctx.db('sec_conversations').where({ id: ctx.conversationId }).update({ status: 'closed', summary, updated_at: new Date(), }); await ctx.db('sec_messages').where({ conversation_id: ctx.conversationId }).delete(); return { ok: true, protocolo: conv?.protocol_number ?? '', resumo: summary }; }, }, ]; function resolveTools(names) { return names .map(n => exports.BUILTIN_TOOLS.find(t => t.name === n)) .filter((t) => t !== undefined); } exports.ALL_TOOL_NAMES = exports.BUILTIN_TOOLS.map(t => t.name); //# sourceMappingURL=tools.js.map