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
@@ -134,6 +134,8 @@ func agentTools() []agentTool {
|
||||
|
||||
return []agentTool{
|
||||
// ── 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)),
|
||||
|
||||
// ── Coolify ──────────────────────────────────────────────────────────
|
||||
@@ -209,7 +211,8 @@ func agentTools() []agentTool {
|
||||
}, 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.",
|
||||
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"),
|
||||
"numero": str("Número de factura, ej: FEV 92"),
|
||||
"descripcion": str("Concepto, ej: 'Hosting por 1 año'"),
|
||||
@@ -236,10 +239,11 @@ func agentTools() []agentTool {
|
||||
}, []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.",
|
||||
obj(map[string]agentToolParam{
|
||||
"cliente_id": num("ID del cliente al que pertenece la factura (usa listar_clientes si no lo sabes)"),
|
||||
"numero": str("Número de factura, si el usuario lo indicó"),
|
||||
"monto": num("Monto de la factura, si el usuario lo indicó"),
|
||||
"descripcion": str("Descripción breve, ej: 'Factura mensual octubre'"),
|
||||
"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."),
|
||||
"numero": str("Número de factura, si el usuario lo indicó"),
|
||||
"monto": num("Monto de la factura, si el usuario lo indicó"),
|
||||
"descripcion": str("Descripción breve, ej: 'Factura mensual octubre'"),
|
||||
}, []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.",
|
||||
obj(map[string]agentToolParam{
|
||||
@@ -277,7 +281,8 @@ func agentTools() []agentTool {
|
||||
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.",
|
||||
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"}},
|
||||
"duracion_meses": num("Duración del contrato en meses (default 12)"),
|
||||
"precio_acordado": num("Valor total acordado"),
|
||||
@@ -315,10 +320,11 @@ func agentTools() []agentTool {
|
||||
}, nil)),
|
||||
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{
|
||||
"cliente_id": num("ID del cliente"),
|
||||
"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
|
||||
"valor": num("Valor a cobrar"),
|
||||
"notas": str("Notas adicionales"),
|
||||
"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."),
|
||||
"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
|
||||
"valor": num("Valor a cobrar"),
|
||||
"notas": str("Notas adicionales"),
|
||||
}, []string{"cliente_id", "descripcion", "valor"})),
|
||||
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"})),
|
||||
@@ -335,9 +341,10 @@ func agentTools() []agentTool {
|
||||
}, 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).",
|
||||
obj(map[string]agentToolParam{
|
||||
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
||||
"alcance": str("Descripción del alcance del proyecto o servicio a cotizar"),
|
||||
"tipo_proyecto": str("Tipo de proyecto, ej: migracion_m365, vm_azure, soporte"),
|
||||
"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."),
|
||||
"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{
|
||||
Type: "array",
|
||||
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"),
|
||||
"propuesta": str("Propuesta técnica ya redactada (HTML o texto) que se insertará en el documento"),
|
||||
"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"},
|
||||
}, []string{"requerimiento", "propuesta"})),
|
||||
|
||||
@@ -374,10 +382,11 @@ func agentTools() []agentTool {
|
||||
}, nil)),
|
||||
tool("crear_proyecto", "Crea un nuevo proyecto para un cliente.",
|
||||
obj(map[string]agentToolParam{
|
||||
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
||||
"nombre": str("Nombre del proyecto"),
|
||||
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
||||
"descripcion": str("Descripción adicional del proyecto"),
|
||||
"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."),
|
||||
"nombre": str("Nombre del proyecto"),
|
||||
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
||||
"descripcion": str("Descripción adicional del proyecto"),
|
||||
}, []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.",
|
||||
obj(map[string]agentToolParam{
|
||||
@@ -512,6 +521,11 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
}
|
||||
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 {
|
||||
if v, ok := a[key]; 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
|
||||
|
||||
case "crear_factura":
|
||||
clienteID := uint(getInt("cliente_id", 0))
|
||||
if clienteID == 0 {
|
||||
return nil, fmt.Errorf("cliente_id requerido")
|
||||
clienteID, err := resolverCliente()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monto := getFloat("monto", 0)
|
||||
if monto <= 0 {
|
||||
@@ -800,9 +814,9 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
if !ok {
|
||||
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))
|
||||
if clienteID == 0 {
|
||||
return nil, fmt.Errorf("cliente_id requerido")
|
||||
clienteID, err := resolverCliente()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := &models.Factura{
|
||||
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
|
||||
|
||||
case "estado_herramientas":
|
||||
return map[string]interface{}{"items": EstadoHerramientas()}, nil
|
||||
|
||||
case "listar_clientes":
|
||||
page := getInt("page", 1)
|
||||
search := getStr("search")
|
||||
@@ -922,8 +939,12 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
servicioIDs = append(servicioIDs, uint(x))
|
||||
}
|
||||
}
|
||||
clienteID, err := resolverCliente()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contrato, doc, err := CrearContratoConDocumento(
|
||||
uint(getInt("cliente_id", 0)),
|
||||
clienteID,
|
||||
servicioIDs,
|
||||
getInt("duracion_meses", 12),
|
||||
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
|
||||
|
||||
case "crear_cuenta_cobro":
|
||||
clienteID, err := resolverCliente()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc, doc, err := CrearCuentaCobroConDocumento(
|
||||
uint(getInt("cliente_id", 0)),
|
||||
clienteID,
|
||||
getStr("descripcion"),
|
||||
getFloat("valor", 0),
|
||||
nil,
|
||||
@@ -1084,7 +1109,11 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
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 {
|
||||
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
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1895,7 +1928,9 @@ Comandos: /reset · /instancias · /ayuda`, nil
|
||||
// guardó algo aunque la herramienta haya devuelto un error, así que
|
||||
// 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))
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user