/** * Secretaria — Tool Definitions * * Cada ToolDef é uma função que a IA pode chamar durante o atendimento. * O motor executa a função, injeta o resultado e chama a IA novamente. * * Tools disponíveis: * listar_horarios — agenda: horários livres * agendar_horario — agenda: reserva um slot * escalar_humano — handoff: transfere para atendente * encerrar_protocolo — fecha o protocolo com resumo */ import { Knex } from 'knex' import type { HookBus } from '../../backend/src/core/hook-bus' // ── Tipos públicos ───────────────────────────────────────────────────────────── export interface ToolParam { type: string description: string enum?: string[] } export interface ToolDef { name: string description: string parameters: { type: 'object' properties: Record required?: string[] } execute: (args: Record, ctx: ToolContext) => Promise } export interface ToolContext { db: Knex conversationId: string extChatId?: string tenantId?: string hooks?: HookBus /** Telemetria cumulativa preenchida pelos tool loops (M1.4). */ _telemetry?: { usage: { input: number; output: number; total: number; cache_read?: number; cached?: number } provider: string model: string iterations: number } } // ── Helpers ──────────────────────────────────────────────────────────────────── function fmtDate(d: Date): string { return d.toISOString().split('T')[0]! } // ── Tools ────────────────────────────────────────────────────────────────────── export const BUILTIN_TOOLS: ToolDef[] = [ // ── 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) { const from = args['data'] ?? fmtDate(new Date()) const to = args['data'] ?? fmtDate(new Date(Date.now() + 7 * 86_400_000)) 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 as any[]).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) { 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 } }, }, ] export function resolveTools(names: string[]): ToolDef[] { return names .map(n => BUILTIN_TOOLS.find(t => t.name === n)) .filter((t): t is ToolDef => t !== undefined) } export const ALL_TOOL_NAMES = BUILTIN_TOOLS.map(t => t.name)