Files
soft_usite/pkg/services/agent_resolver.go
T
Lizandro GuarnizoandClaude Sonnet 5 e2440b04f1 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>
2026-08-15 21:24:06 -05:00

122 lines
3.9 KiB
Go

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
}