Files
soft_usite/pkg/services/telegram_tools_test.go
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

100 lines
3.6 KiB
Go

package services
import (
"strings"
"testing"
)
// Registrar una factura dictando los datos no era posible: la única
// herramienta de venta era adjuntar_factura, que exige un archivo pendiente.
// Sin una alternativa, el modelo no tenía con qué guardar y el usuario recibía
// un "guardado exitosamente" sobre algo que nunca se creó.
func TestExisteHerramientaParaFacturaSinAdjunto(t *testing.T) {
tools := agentTools()
nombres := map[string]agentTool{}
for _, tl := range tools {
nombres[tl.Function.Name] = tl
}
crear, ok := nombres["crear_factura"]
if !ok {
t.Fatal("falta crear_factura: no habría forma de registrar una factura de venta sin archivo adjunto")
}
if _, ok := nombres["adjuntar_factura"]; !ok {
t.Error("adjuntar_factura debe seguir existiendo para el caso con documento")
}
// La descripción tiene que distinguirlas, o el modelo elige la equivocada
// y falla por falta de adjunto — que es justo el problema original.
d := strings.ToLower(crear.Function.Description)
if !strings.Contains(d, "sin archivo adjunto") {
t.Error("la descripción debe dejar claro que no necesita adjunto")
}
if !strings.Contains(d, "adjuntar_factura") {
t.Error("la descripción debe remitir a adjuntar_factura cuando sí hay documento")
}
// cliente_id y monto son obligatorios: sin ellos la factura no sirve.
req := strings.Join(crear.Function.Parameters.Required, ",")
for _, campo := range []string{"cliente_id", "monto"} {
if !strings.Contains(req, campo) {
t.Errorf("%q debería ser obligatorio en crear_factura", campo)
}
}
}
// El modelo puede informar éxito aunque la herramienta haya fallado — pasó con
// una factura que nunca se creó. El aviso lo escribe el código a partir del
// resultado real, así que no depende de que el modelo se porte bien.
func TestErrorDeTool(t *testing.T) {
casos := []struct {
nombre string
resultado string
esperado string
}{
{"error de la herramienta", `{"error": "cliente_id requerido"}`, "cliente_id requerido"},
{"resultado exitoso", `{"ok": true, "factura_id": 12}`, ""},
{"lista de datos", `{"items": [], "total": 0}`, ""},
{"resultado no JSON", `algo suelto`, ""},
{"error vacío no cuenta", `{"error": ""}`, ""},
}
for _, cas := range casos {
if got := errorDeTool(cas.resultado); got != cas.esperado {
t.Errorf("%s: errorDeTool(%s) = %q, esperaba %q", cas.nombre, cas.resultado, got, cas.esperado)
}
}
}
// 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)
}
}
}