chore(ops): restore all source files in newwhats.clube67.com
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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<string, ToolParam>
|
||||
required?: string[]
|
||||
}
|
||||
execute: (args: Record<string, any>, ctx: ToolContext) => Promise<any>
|
||||
}
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user