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>
100 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|