feat(hermes): 4 herramientas nuevas + índices DB

Hermes ahora puede:
- buscar_leads: buscar leads por nombre/teléfono/email
- ver_cliente: info completa de un cliente
- ver_oportunidades: pipeline de ventas con filtro por etapa
- ver_cobranza: resumen de pagos vencidos

Índices DB: LeadCaptura.nombreLead, Cliente.nombreCompleto, PlanPago.status
38 tests nuevos pasan.
This commit is contained in:
2026-09-10 21:11:01 +00:00
parent 88babe9913
commit 44360591b0
3 changed files with 378 additions and 2 deletions

View File

@@ -255,6 +255,12 @@ export function construirSystemPrompt(contexto: ChunkRecuperado[]): string {
'- Cuando uses cotizar_unidad, presenta enganche, mensualidades y pago final, y aclara que la',
' cotización es ORIENTATIVA: el PDF oficial se genera en el cotizador (/quote/).',
'- Si la unidad no existe o no está DISPONIBLE, dilo claramente y ofrece buscar alternativas.',
'- Tienes acceso a la base de datos de clientes, leads y cobranza.',
'- Usa buscar_leads cuando te pidan información sobre un lead específico.',
'- Usa ver_cliente para obtener datos completos de un cliente.',
'- Usa ver_oportunidades para ver el pipeline de ventas.',
'- Usa ver_cobranza para información de pagos y adeudos.',
'- NUNCA inventes datos de clientes o pagos; usa siempre las herramientas.',
'',
contexto.length > 0
? `CONTEXTO:\n\n${bloques}`
@@ -384,6 +390,65 @@ export const HERRAMIENTAS: HerramientaDef[] = [
},
},
},
{
type: 'function',
function: {
name: 'buscar_leads',
description:
'Busca leads por nombre, teléfono o email. Devuelve hasta 10 resultados con nombre, teléfono, email, estado, origen, desarrollo.',
parameters: {
type: 'object',
properties: {
busqueda: { type: 'string', description: 'Texto de búsqueda (nombre, teléfono o email)' },
},
required: ['busqueda'],
},
},
},
{
type: 'function',
function: {
name: 'ver_cliente',
description:
'Muestra la información completa de un cliente: datos personales, propiedad, cotización, pagos. Requiere el nombre del cliente.',
parameters: {
type: 'object',
properties: {
nombre: { type: 'string', description: 'Nombre del cliente (búsqueda parcial)' },
},
required: ['nombre'],
},
},
},
{
type: 'function',
function: {
name: 'ver_oportunidades',
description:
'Lista las oportunidades activas con su etapa, cliente, desarrollo y teléfono. Opcionalmente filtra por etapa.',
parameters: {
type: 'object',
properties: {
etapa: {
type: 'string',
description: 'Filtrar por etapa: Afluencia, Agendo Cita, Cotizador, Aparto',
},
},
},
},
},
{
type: 'function',
function: {
name: 'ver_cobranza',
description:
'Muestra el estado de cobranza: clientes con pagos vencidos, monto total adeudado, planes activos.',
parameters: {
type: 'object',
properties: {},
},
},
},
]
export interface ArgsCotizarUnidad {
@@ -399,9 +464,25 @@ export interface ArgsBuscarUnidad {
precioMax?: number
}
export interface ArgsBuscarLeads {
busqueda: string
}
export interface ArgsVerCliente {
nombre: string
}
export interface ArgsVerOportunidades {
etapa?: string
}
export interface EjecutoresHerramientas {
cotizar_unidad(args: ArgsCotizarUnidad): Promise<unknown>
buscar_unidad(args: ArgsBuscarUnidad): Promise<unknown>
buscar_leads(args: ArgsBuscarLeads): Promise<unknown>
ver_cliente(args: ArgsVerCliente): Promise<unknown>
ver_oportunidades(args: ArgsVerOportunidades): Promise<unknown>
ver_cobranza(): Promise<unknown>
}
/** Redondeo monetario a 2 decimales para los números que lee el modelo */
@@ -471,6 +552,122 @@ export function crearEjecutoresHerramientas(
supConstruccion: u.supConstruccion,
}))
},
async buscar_leads(args) {
const termino = (args.busqueda ?? '').trim()
if (!termino) return { error: 'Falta el texto de búsqueda' }
const leads = await prisma.leadCaptura.findMany({
where: {
OR: [
{ nombreLead: { contains: termino } },
{ telefono: { contains: termino } },
{ email: { contains: termino } },
],
},
take: 10,
orderBy: { createdAt: 'desc' },
})
return leads.map((l) => ({
nombre: l.nombreLead,
telefono: l.telefono,
email: l.email,
estado: l.etapa,
origen: l.origen,
desarrollo: l.desarrollo,
board: l.board,
}))
},
async ver_cliente(args) {
const nombre = (args.nombre ?? '').trim()
if (!nombre) return { error: 'Falta el nombre del cliente' }
const cliente = await prisma.cliente.findFirst({
where: { nombreCompleto: { contains: nombre } },
})
if (!cliente) return { error: `No se encontró ningún cliente con nombre "${nombre}"` }
const [leads, cotizaciones] = await Promise.all([
prisma.leadCaptura.findMany({
where: { nombreLead: { contains: cliente.nombreCompleto } },
orderBy: { createdAt: 'desc' },
take: 5,
}),
prisma.cotizacion.findMany({
where: { clienteNombre: { contains: cliente.nombreCompleto } },
orderBy: { createdAt: 'desc' },
take: 10,
}),
])
return {
id: cliente.id,
nombre: cliente.nombreCompleto,
curp: cliente.curp,
rfc: cliente.rfc,
email: cliente.email,
celular: cliente.celular,
proyecto: cliente.proyecto,
unidadCodigo: cliente.unidadCodigo,
lead: leads.length > 0
? {
etapa: leads[0]!.etapa,
board: leads[0]!.board,
origen: leads[0]!.origen,
}
: null,
cotizaciones: cotizaciones.map((c) => ({
unidad: c.unidadCodigo,
tipoPropuesta: c.tipoPropuesta,
precioFinal: r2(c.precioFinal),
enganche: r2(c.enganche),
mensualidad: r2(c.mensualidad),
createdAt: c.createdAt,
})),
}
},
async ver_oportunidades(args) {
const where: Record<string, unknown> = { board: 'OPORTUNIDADES' }
if (args.etapa) where.etapa = args.etapa.trim()
const oportunidades = await prisma.leadCaptura.findMany({
where,
take: 20,
orderBy: { createdAt: 'desc' },
})
return oportunidades.map((o) => ({
nombre: o.nombreLead,
etapa: o.etapa,
desarrollo: o.desarrollo,
telefono: o.telefono,
email: o.email,
}))
},
async ver_cobranza() {
const [planesActivos, planesCancelados, todosPagos] = await Promise.all([
prisma.planPago.count({ where: { status: 'ACTIVO' } }),
prisma.planPago.count({ where: { status: 'CANCELADO' } }),
prisma.pago.findMany({
where: { fechaVencimiento: { lt: new Date() } },
select: {
monto: true,
montoPagado: true,
fechaVencimiento: true,
plan: { select: { clienteNombre: true, unidadCodigo: true } },
},
}),
])
const pagosVencidos = todosPagos.filter((p) => p.montoPagado < p.monto)
const montoAdeudado = pagosVencidos.reduce((sum, p) => sum + (p.monto - p.montoPagado), 0)
return {
planesActivos,
planesCancelados,
pagosVencidos: pagosVencidos.length,
montoAdeudado: r2(montoAdeudado),
detalle: pagosVencidos.slice(0, 10).map((p) => ({
cliente: p.plan.clienteNombre,
unidad: p.plan.unidadCodigo,
montoAdeudado: r2(p.monto - p.montoPagado),
vencimiento: p.fechaVencimiento,
})),
}
},
}
}
@@ -488,7 +685,7 @@ export async function ejecutarToolCall(
const ejecutor = ejecutores[call.nombre as keyof EjecutoresHerramientas]
if (!ejecutor) return JSON.stringify({ error: `Herramienta desconocida: ${call.nombre}` })
try {
return JSON.stringify(await ejecutor(args))
return JSON.stringify(await (ejecutor as (a: Record<string, unknown>) => Promise<unknown>)(args))
} catch (err) {
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
}