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 }