feat(telegram): el bot resuelve clientes por nombre y mide sus propios fallos
Dos de las cuatro mejoras de la auditoría del agente. 1. Las herramientas aceptan cliente_nombre además de cliente_id. De 27 parámetros que eran IDs, 10 eran de cliente. Para cada acción el modelo tenía que encadenar: listar_clientes → leer la respuesta → encontrar la fila → extraer el número → recién ahí llamar a la herramienta. Cuatro pasos, y basta que falle uno para que no se guarde nada — es la explicación más probable de la factura que nunca se creó, porque ese cliente podía no venir en la primera página del listado. Ahora el modelo pasa el nombre y el servidor lo resuelve, con búsqueda completa y no paginada. Si hay varios candidatos devuelve la lista para que el modelo pregunte cuál, en vez de elegir uno: cargarle una factura a la empresa equivocada es peor que preguntar. Una coincidencia exacta gana sobre las parciales, así "Metropolitana" no queda ambiguo solo porque existe "Metropolitana Norte". 2. Contadores de uso y fallo por herramienta, expuestos como estado_herramientas. Cada problema se venía diagnosticando de a un caso, reconstruyendo qué pasó después de que el usuario avisara. Ahora se puede preguntar directamente qué viene fallando y ver si es una herramienta puntual o el modelo eligiendo mal. En memoria a propósito: alcanza para responder eso y no agrega una tabla. Quedan las otras dos: adelgazar el prompt de 5,4 KB moviendo los manuales por dominio a las descripciones de las herramientas, y comparar modelos con un mismo caso. La primera conviene hacerla con el sistema estable, porque toca cómo decide el modelo en todos los flujos a la vez. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d0c9ed5656
commit
e2440b04f1
@@ -0,0 +1,121 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolución de entidades por nombre para el agente de Telegram.
|
||||||
|
//
|
||||||
|
// La mayoría de las herramientas pedían un ID numérico, y el modelo tenía que
|
||||||
|
// conseguirlo antes: llamar a listar_*, leer la respuesta, encontrar la fila
|
||||||
|
// correcta y extraer el número. Cuatro pasos encadenados por cada acción, y
|
||||||
|
// basta que falle uno para que no se guarde nada — es lo que pasó con una
|
||||||
|
// factura cuyo cliente no salía en la primera página del listado.
|
||||||
|
//
|
||||||
|
// Aceptando el nombre y resolviéndolo acá, esa cadena desaparece: el modelo
|
||||||
|
// pasa lo que el usuario dijo y el servidor hace la búsqueda, que además es
|
||||||
|
// exacta y no depende de que el listado estuviera paginado.
|
||||||
|
|
||||||
|
// ResolverClienteID devuelve el ID a partir de un id explícito o de un nombre.
|
||||||
|
//
|
||||||
|
// Si el nombre coincide con varios clientes devuelve un error que los enumera:
|
||||||
|
// es mejor que el modelo pregunte cuál a que elija uno al azar y la factura
|
||||||
|
// termine cargada a otra empresa.
|
||||||
|
func ResolverClienteID(id uint, nombre string) (uint, error) {
|
||||||
|
if id > 0 {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
nombre = strings.TrimSpace(nombre)
|
||||||
|
if nombre == "" {
|
||||||
|
return 0, fmt.Errorf("indicá el cliente: su nombre o su id")
|
||||||
|
}
|
||||||
|
|
||||||
|
items, _, err := models.GetAllClientes(50, 0, nombre)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch len(items) {
|
||||||
|
case 0:
|
||||||
|
return 0, fmt.Errorf("no encontré ningún cliente que coincida con %q; revisá el nombre o creálo con crear_cliente", nombre)
|
||||||
|
case 1:
|
||||||
|
return items[0].ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Una coincidencia exacta gana sobre las parciales: "Metropolitana" no
|
||||||
|
// debería quedar ambiguo solo porque existe "Metropolitana Norte".
|
||||||
|
buscado := strings.ToLower(nombre)
|
||||||
|
for _, c := range items {
|
||||||
|
if strings.ToLower(strings.TrimSpace(c.Nombre)) == buscado ||
|
||||||
|
strings.ToLower(strings.TrimSpace(c.Empresa)) == buscado {
|
||||||
|
return c.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var opciones []string
|
||||||
|
for i, c := range items {
|
||||||
|
if i == 8 {
|
||||||
|
opciones = append(opciones, fmt.Sprintf("y %d más", len(items)-8))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
etiqueta := c.Nombre
|
||||||
|
if c.Empresa != "" && !strings.EqualFold(c.Empresa, c.Nombre) {
|
||||||
|
etiqueta = fmt.Sprintf("%s (%s)", c.Nombre, c.Empresa)
|
||||||
|
}
|
||||||
|
opciones = append(opciones, fmt.Sprintf("%d = %s", c.ID, etiqueta))
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("hay varios clientes que coinciden con %q, preguntale al usuario cuál: %s",
|
||||||
|
nombre, strings.Join(opciones, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Salud de las herramientas ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Contadores en memoria de uso y fallo por herramienta.
|
||||||
|
//
|
||||||
|
// Hasta ahora cada problema se diagnosticaba de a un caso: el usuario avisaba
|
||||||
|
// que algo no se guardó y había que reconstruir qué pasó. Con esto se ve de
|
||||||
|
// una si el fallo es de una herramienta puntual o del modelo eligiendo mal.
|
||||||
|
//
|
||||||
|
// ponytail: en memoria, se reinicia con el proceso. Alcanza para responder
|
||||||
|
// "¿qué está fallando esta semana?"; si hiciera falta histórico, va a tabla.
|
||||||
|
var (
|
||||||
|
agentStatsMu sync.Mutex
|
||||||
|
agentUsos = map[string]int{}
|
||||||
|
agentFallos = map[string]int{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegistrarUsoHerramienta(nombre string, fallo bool) {
|
||||||
|
agentStatsMu.Lock()
|
||||||
|
defer agentStatsMu.Unlock()
|
||||||
|
agentUsos[nombre]++
|
||||||
|
if fallo {
|
||||||
|
agentFallos[nombre]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstadoHerramientas devuelve el resumen ordenado por cantidad de fallos.
|
||||||
|
func EstadoHerramientas() []map[string]interface{} {
|
||||||
|
agentStatsMu.Lock()
|
||||||
|
defer agentStatsMu.Unlock()
|
||||||
|
|
||||||
|
out := make([]map[string]interface{}, 0, len(agentUsos))
|
||||||
|
for nombre, usos := range agentUsos {
|
||||||
|
out = append(out, map[string]interface{}{
|
||||||
|
"herramienta": nombre,
|
||||||
|
"usos": usos,
|
||||||
|
"fallos": agentFallos[nombre],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
fi, fj := out[i]["fallos"].(int), out[j]["fallos"].(int)
|
||||||
|
if fi != fj {
|
||||||
|
return fi > fj
|
||||||
|
}
|
||||||
|
return out[i]["usos"].(int) > out[j]["usos"].(int)
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -134,6 +134,8 @@ func agentTools() []agentTool {
|
|||||||
|
|
||||||
return []agentTool{
|
return []agentTool{
|
||||||
// ── Sistema ──────────────────────────────────────────────────────────
|
// ── Sistema ──────────────────────────────────────────────────────────
|
||||||
|
tool("estado_herramientas", "Muestra qué herramientas se usaron y cuáles fallaron desde que arrancó el sistema. Útil cuando el usuario pregunta por qué algo no se guardó o si algo viene fallando.",
|
||||||
|
obj(map[string]agentToolParam{}, nil)),
|
||||||
tool("sistema_info", "Información general del sistema y recursos disponibles.", obj(nil, nil)),
|
tool("sistema_info", "Información general del sistema y recursos disponibles.", obj(nil, nil)),
|
||||||
|
|
||||||
// ── Coolify ──────────────────────────────────────────────────────────
|
// ── Coolify ──────────────────────────────────────────────────────────
|
||||||
@@ -209,7 +211,8 @@ func agentTools() []agentTool {
|
|||||||
}, nil)),
|
}, nil)),
|
||||||
tool("crear_factura", "Registra una factura de VENTA a partir de datos escritos, SIN archivo adjunto: U-SITE le factura/cobra al cliente. Usa esta cuando el usuario dicta los datos de la factura. Si en cambio acaba de enviar un PDF o una foto de la factura, usa adjuntar_factura.",
|
tool("crear_factura", "Registra una factura de VENTA a partir de datos escritos, SIN archivo adjunto: U-SITE le factura/cobra al cliente. Usa esta cuando el usuario dicta los datos de la factura. Si en cambio acaba de enviar un PDF o una foto de la factura, usa adjuntar_factura.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"cliente_id": num("ID del cliente al que se le factura (usa listar_clientes si no lo sabes)"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"monto": num("Monto total de la factura"),
|
"monto": num("Monto total de la factura"),
|
||||||
"numero": str("Número de factura, ej: FEV 92"),
|
"numero": str("Número de factura, ej: FEV 92"),
|
||||||
"descripcion": str("Concepto, ej: 'Hosting por 1 año'"),
|
"descripcion": str("Concepto, ej: 'Hosting por 1 año'"),
|
||||||
@@ -236,10 +239,11 @@ func agentTools() []agentTool {
|
|||||||
}, []string{"cuenta_id"})),
|
}, []string{"cuenta_id"})),
|
||||||
tool("adjuntar_factura", "Guarda como factura de VENTA el documento (PDF/foto) que el usuario acaba de enviar por Telegram: U-SITE es quien factura/cobra al cliente. Solo funciona si hay un archivo adjunto pendiente. Si el documento es al revés (un proveedor le factura a U-SITE), usa adjuntar_factura_compra en su lugar, no esta.",
|
tool("adjuntar_factura", "Guarda como factura de VENTA el documento (PDF/foto) que el usuario acaba de enviar por Telegram: U-SITE es quien factura/cobra al cliente. Solo funciona si hay un archivo adjunto pendiente. Si el documento es al revés (un proveedor le factura a U-SITE), usa adjuntar_factura_compra en su lugar, no esta.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"cliente_id": num("ID del cliente al que pertenece la factura (usa listar_clientes si no lo sabes)"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
"numero": str("Número de factura, si el usuario lo indicó"),
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"monto": num("Monto de la factura, si el usuario lo indicó"),
|
"numero": str("Número de factura, si el usuario lo indicó"),
|
||||||
"descripcion": str("Descripción breve, ej: 'Factura mensual octubre'"),
|
"monto": num("Monto de la factura, si el usuario lo indicó"),
|
||||||
|
"descripcion": str("Descripción breve, ej: 'Factura mensual octubre'"),
|
||||||
}, []string{"cliente_id"})),
|
}, []string{"cliente_id"})),
|
||||||
tool("adjuntar_factura_compra", "Guarda como factura de COMPRA (cuenta por pagar) el documento que el usuario acaba de enviar por Telegram: un proveedor le está facturando a U-SITE, no al revés. Busca o crea el proveedor por nombre. Solo funciona si hay un archivo adjunto pendiente.",
|
tool("adjuntar_factura_compra", "Guarda como factura de COMPRA (cuenta por pagar) el documento que el usuario acaba de enviar por Telegram: un proveedor le está facturando a U-SITE, no al revés. Busca o crea el proveedor por nombre. Solo funciona si hay un archivo adjunto pendiente.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
@@ -277,7 +281,8 @@ func agentTools() []agentTool {
|
|||||||
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
||||||
tool("crear_contrato", "Crea un contrato para un cliente con uno o más servicios y genera de una vez su PDF con cláusulas estándar.",
|
tool("crear_contrato", "Crea un contrato para un cliente con uno o más servicios y genera de una vez su PDF con cláusulas estándar.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"servicio_ids": agentToolParam{Type: "array", Description: "IDs de los servicios contratados", Items: &agentToolParam{Type: "number"}},
|
"servicio_ids": agentToolParam{Type: "array", Description: "IDs de los servicios contratados", Items: &agentToolParam{Type: "number"}},
|
||||||
"duracion_meses": num("Duración del contrato en meses (default 12)"),
|
"duracion_meses": num("Duración del contrato en meses (default 12)"),
|
||||||
"precio_acordado": num("Valor total acordado"),
|
"precio_acordado": num("Valor total acordado"),
|
||||||
@@ -315,10 +320,11 @@ func agentTools() []agentTool {
|
|||||||
}, nil)),
|
}, nil)),
|
||||||
tool("crear_cuenta_cobro", "Crea una cuenta de cobro para un cliente/proyecto y genera de una vez su PDF de solicitud.",
|
tool("crear_cuenta_cobro", "Crea una cuenta de cobro para un cliente/proyecto y genera de una vez su PDF de solicitud.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"cliente_id": num("ID del cliente"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"valor": num("Valor a cobrar"),
|
"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
|
||||||
"notas": str("Notas adicionales"),
|
"valor": num("Valor a cobrar"),
|
||||||
|
"notas": str("Notas adicionales"),
|
||||||
}, []string{"cliente_id", "descripcion", "valor"})),
|
}, []string{"cliente_id", "descripcion", "valor"})),
|
||||||
tool("generar_documento_cuenta_cobro", "Genera (o regenera) el PDF de una cuenta de cobro ya existente.",
|
tool("generar_documento_cuenta_cobro", "Genera (o regenera) el PDF de una cuenta de cobro ya existente.",
|
||||||
obj(map[string]agentToolParam{"id": num("ID de la cuenta de cobro")}, []string{"id"})),
|
obj(map[string]agentToolParam{"id": num("ID de la cuenta de cobro")}, []string{"id"})),
|
||||||
@@ -335,9 +341,10 @@ func agentTools() []agentTool {
|
|||||||
}, nil)),
|
}, nil)),
|
||||||
tool("crear_cotizacion", "Genera el PDF de una cotización para un cliente, a partir de la plantilla activa y los items con precio ya calculado (consulta listar_tarifas antes para saber los valores).",
|
tool("crear_cotizacion", "Genera el PDF de una cotización para un cliente, a partir de la plantilla activa y los items con precio ya calculado (consulta listar_tarifas antes para saber los valores).",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
"alcance": str("Descripción del alcance del proyecto o servicio a cotizar"),
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"tipo_proyecto": str("Tipo de proyecto, ej: migracion_m365, vm_azure, soporte"),
|
"alcance": str("Descripción del alcance del proyecto o servicio a cotizar"),
|
||||||
|
"tipo_proyecto": str("Tipo de proyecto, ej: migracion_m365, vm_azure, soporte"),
|
||||||
"items": agentToolParam{
|
"items": agentToolParam{
|
||||||
Type: "array",
|
Type: "array",
|
||||||
Description: "Ítems de la cotización con precio ya calculado",
|
Description: "Ítems de la cotización con precio ya calculado",
|
||||||
@@ -362,7 +369,8 @@ func agentTools() []agentTool {
|
|||||||
"requerimiento": str("Requerimiento del cliente en texto libre"),
|
"requerimiento": str("Requerimiento del cliente en texto libre"),
|
||||||
"propuesta": str("Propuesta técnica ya redactada (HTML o texto) que se insertará en el documento"),
|
"propuesta": str("Propuesta técnica ya redactada (HTML o texto) que se insertará en el documento"),
|
||||||
"nombre": str("Título de la propuesta"),
|
"nombre": str("Título de la propuesta"),
|
||||||
"cliente_id": num("ID del cliente (opcional)"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"guardar_como_referencia": agentToolParam{Type: "boolean", Description: "Si true, guarda esta propuesta como patrón reutilizable para el futuro"},
|
"guardar_como_referencia": agentToolParam{Type: "boolean", Description: "Si true, guarda esta propuesta como patrón reutilizable para el futuro"},
|
||||||
}, []string{"requerimiento", "propuesta"})),
|
}, []string{"requerimiento", "propuesta"})),
|
||||||
|
|
||||||
@@ -374,10 +382,11 @@ func agentTools() []agentTool {
|
|||||||
}, nil)),
|
}, nil)),
|
||||||
tool("crear_proyecto", "Crea un nuevo proyecto para un cliente.",
|
tool("crear_proyecto", "Crea un nuevo proyecto para un cliente.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
"cliente_id": num("ID del cliente, si ya lo conocés"),
|
||||||
"nombre": str("Nombre del proyecto"),
|
"cliente_nombre": str("Nombre o empresa del cliente, tal como lo dijo el usuario. Alcanza con esto: el servidor lo resuelve. Usalo en vez de buscar el id con listar_clientes."),
|
||||||
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
"nombre": str("Nombre del proyecto"),
|
||||||
"descripcion": str("Descripción adicional del proyecto"),
|
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
||||||
|
"descripcion": str("Descripción adicional del proyecto"),
|
||||||
}, []string{"cliente_id", "nombre"})),
|
}, []string{"cliente_id", "nombre"})),
|
||||||
tool("crear_fase_proyecto", "Agrega una fase/etapa a un proyecto existente (ej: Diseño, Desarrollo, QA, Entrega). Se puede llamar varias veces seguidas para registrar todas las fases de un proyecto de una vez.",
|
tool("crear_fase_proyecto", "Agrega una fase/etapa a un proyecto existente (ej: Diseño, Desarrollo, QA, Entrega). Se puede llamar varias veces seguidas para registrar todas las fases de un proyecto de una vez.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
@@ -512,6 +521,11 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
// Resuelve el cliente por id o por nombre. Ver agent_resolver.go: evita que
|
||||||
|
// el modelo tenga que encadenar listar_clientes → leer → extraer id.
|
||||||
|
resolverCliente := func() (uint, error) {
|
||||||
|
return ResolverClienteID(uint(getInt("cliente_id", 0)), getStr("cliente_nombre"))
|
||||||
|
}
|
||||||
getBool := func(key string) bool {
|
getBool := func(key string) bool {
|
||||||
if v, ok := a[key]; ok {
|
if v, ok := a[key]; ok {
|
||||||
if b, ok := v.(bool); ok {
|
if b, ok := v.(bool); ok {
|
||||||
@@ -691,9 +705,9 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||||
|
|
||||||
case "crear_factura":
|
case "crear_factura":
|
||||||
clienteID := uint(getInt("cliente_id", 0))
|
clienteID, err := resolverCliente()
|
||||||
if clienteID == 0 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cliente_id requerido")
|
return nil, err
|
||||||
}
|
}
|
||||||
monto := getFloat("monto", 0)
|
monto := getFloat("monto", 0)
|
||||||
if monto <= 0 {
|
if monto <= 0 {
|
||||||
@@ -800,9 +814,9 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo")
|
return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo")
|
||||||
}
|
}
|
||||||
clienteID := uint(getInt("cliente_id", 0))
|
clienteID, err := resolverCliente()
|
||||||
if clienteID == 0 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cliente_id requerido")
|
return nil, err
|
||||||
}
|
}
|
||||||
f := &models.Factura{
|
f := &models.Factura{
|
||||||
ClienteID: clienteID,
|
ClienteID: clienteID,
|
||||||
@@ -838,6 +852,9 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
}
|
}
|
||||||
return map[string]interface{}{"ok": true, "cuenta_pagar_id": cp.ID, "proveedor_id": cp.EntidadID}, nil
|
return map[string]interface{}{"ok": true, "cuenta_pagar_id": cp.ID, "proveedor_id": cp.EntidadID}, nil
|
||||||
|
|
||||||
|
case "estado_herramientas":
|
||||||
|
return map[string]interface{}{"items": EstadoHerramientas()}, nil
|
||||||
|
|
||||||
case "listar_clientes":
|
case "listar_clientes":
|
||||||
page := getInt("page", 1)
|
page := getInt("page", 1)
|
||||||
search := getStr("search")
|
search := getStr("search")
|
||||||
@@ -922,8 +939,12 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
servicioIDs = append(servicioIDs, uint(x))
|
servicioIDs = append(servicioIDs, uint(x))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clienteID, err := resolverCliente()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
contrato, doc, err := CrearContratoConDocumento(
|
contrato, doc, err := CrearContratoConDocumento(
|
||||||
uint(getInt("cliente_id", 0)),
|
clienteID,
|
||||||
servicioIDs,
|
servicioIDs,
|
||||||
getInt("duracion_meses", 12),
|
getInt("duracion_meses", 12),
|
||||||
float64(getInt("precio_acordado", 0)),
|
float64(getInt("precio_acordado", 0)),
|
||||||
@@ -1004,8 +1025,12 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||||
|
|
||||||
case "crear_cuenta_cobro":
|
case "crear_cuenta_cobro":
|
||||||
|
clienteID, err := resolverCliente()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cc, doc, err := CrearCuentaCobroConDocumento(
|
cc, doc, err := CrearCuentaCobroConDocumento(
|
||||||
uint(getInt("cliente_id", 0)),
|
clienteID,
|
||||||
getStr("descripcion"),
|
getStr("descripcion"),
|
||||||
getFloat("valor", 0),
|
getFloat("valor", 0),
|
||||||
nil,
|
nil,
|
||||||
@@ -1084,7 +1109,11 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
Unidad: toStr(m["unidad"]),
|
Unidad: toStr(m["unidad"]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
doc, total, err := CrearCotizacion(uint(getInt("cliente_id", 0)), getStr("alcance"), getStr("tipo_proyecto"), items, "telegram")
|
clienteID, err := resolverCliente()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
doc, total, err := CrearCotizacion(clienteID, getStr("alcance"), getStr("tipo_proyecto"), items, "telegram")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1132,7 +1161,11 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
|||||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||||
|
|
||||||
case "crear_proyecto":
|
case "crear_proyecto":
|
||||||
p, err := CrearProyectoSimple(uint(getInt("cliente_id", 0)), getStr("nombre"), getStr("stack"), getStr("descripcion"))
|
clienteID, err := resolverCliente()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p, err := CrearProyectoSimple(clienteID, getStr("nombre"), getStr("stack"), getStr("descripcion"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1895,7 +1928,9 @@ Comandos: /reset · /instancias · /ayuda`, nil
|
|||||||
// guardó algo aunque la herramienta haya devuelto un error, así que
|
// guardó algo aunque la herramienta haya devuelto un error, así que
|
||||||
// el log es la única fuente confiable de qué pasó de verdad.
|
// el log es la única fuente confiable de qué pasó de verdad.
|
||||||
log.Printf("[AGENT] Resultado de %s: %s", tc.Function.Name, recortar(toolResult, 500))
|
log.Printf("[AGENT] Resultado de %s: %s", tc.Function.Name, recortar(toolResult, 500))
|
||||||
if msg := errorDeTool(toolResult); msg != "" {
|
msg := errorDeTool(toolResult)
|
||||||
|
RegistrarUsoHerramienta(tc.Function.Name, msg != "")
|
||||||
|
if msg != "" {
|
||||||
fallidas = append(fallidas, fmt.Sprintf("%s: %s", tc.Function.Name, msg))
|
fallidas = append(fallidas, fmt.Sprintf("%s: %s", tc.Function.Name, msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,3 +65,35 @@ func TestErrorDeTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// El id explícito manda: si el modelo ya lo tiene, no hay que ir a la base.
|
||||||
|
// Es el único camino del resolvedor que se puede probar sin datos.
|
||||||
|
func TestResolverClienteIDPrefiereElIDExplicito(t *testing.T) {
|
||||||
|
id, err := ResolverClienteID(42, "cualquier nombre")
|
||||||
|
if err != nil || id != 42 {
|
||||||
|
t.Fatalf("= (%d, %v), esperaba (42, nil)", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sin id ni nombre no se puede adivinar: mejor un error claro que buscar en la
|
||||||
|
// base con la cadena vacía y traer un cliente al azar.
|
||||||
|
func TestResolverClienteIDSinDatos(t *testing.T) {
|
||||||
|
if _, err := ResolverClienteID(0, " "); err == nil {
|
||||||
|
t.Fatal("esperaba error cuando no hay ni id ni nombre")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Las herramientas que piden cliente deben aceptar también el nombre: es lo
|
||||||
|
// que elimina el ciclo listar_clientes → leer → extraer id, que es donde se
|
||||||
|
// perdía la factura.
|
||||||
|
func TestHerramientasDeClienteAceptanNombre(t *testing.T) {
|
||||||
|
for _, tl := range agentTools() {
|
||||||
|
props := tl.Function.Parameters.Properties
|
||||||
|
if _, pide := props["cliente_id"]; !pide {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, acepta := props["cliente_nombre"]; !acepta {
|
||||||
|
t.Errorf("%s pide cliente_id pero no acepta cliente_nombre: obliga al modelo a resolver el id por su cuenta", tl.Function.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user