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:
@@ -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) })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user