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>
122 lines
3.9 KiB
Go
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
|
|
}
|