diff --git a/pkg/services/telegram_agent_service.go b/pkg/services/telegram_agent_service.go index 5652c84..e0c4049 100644 --- a/pkg/services/telegram_agent_service.go +++ b/pkg/services/telegram_agent_service.go @@ -1859,6 +1859,9 @@ Comandos: /reset · /instancias · /ayuda`, nil // Loop de function calling (máximo 6 rondas) var finalResponse string + // Herramientas que devolvieron error en esta conversación, para avisarlo al + // final aunque el modelo diga otra cosa. + var fallidas []string for round := 0; round < 6; round++ { aiMsg, _, err := callAI(ai, messages, tools) if err != nil { @@ -1892,6 +1895,9 @@ Comandos: /reset · /instancias · /ayuda`, nil // guardó algo aunque la herramienta haya devuelto un error, así que // el log es la única fuente confiable de qué pasó de verdad. log.Printf("[AGENT] Resultado de %s: %s", tc.Function.Name, recortar(toolResult, 500)) + if msg := errorDeTool(toolResult); msg != "" { + fallidas = append(fallidas, fmt.Sprintf("%s: %s", tc.Function.Name, msg)) + } // Agregar resultado al contexto (solo en memoria, no en BD) messages = append(messages, agentMessage{ @@ -1906,9 +1912,32 @@ Comandos: /reset · /instancias · /ayuda`, nil if finalResponse == "" { finalResponse = "El agente completó las acciones solicitadas." } + + // Aviso determinista, escrito por el código y no por el modelo. + // + // El modelo puede informar "guardado exitosamente" aunque la herramienta + // haya devuelto un error — ya pasó con una factura que nunca se creó. El + // prompt se lo prohíbe, pero un prompt es una sugerencia: esto es la + // garantía. Si algo falló, se ve, diga lo que diga el modelo. + if len(fallidas) > 0 { + finalResponse += "\n\n⚠️ No se completó: " + strings.Join(fallidas, " · ") + } return finalResponse, nil } +// errorDeTool devuelve el mensaje si el resultado de una herramienta fue un +// error, o "" si salió bien. executeAgentTool serializa los fallos como +// {"error": "..."}, así que alcanza con mirar ese campo. +func errorDeTool(resultado string) string { + var r struct { + Error string `json:"error"` + } + if err := json.Unmarshal([]byte(resultado), &r); err != nil { + return "" + } + return r.Error +} + // recortar limita lo que se escribe al log: un resultado de tool puede traer // listados enteros y no vale la pena volcarlos completos. func recortar(s string, n int) string { diff --git a/pkg/services/telegram_tools_test.go b/pkg/services/telegram_tools_test.go index 2585f76..101555a 100644 --- a/pkg/services/telegram_tools_test.go +++ b/pkg/services/telegram_tools_test.go @@ -43,3 +43,25 @@ func TestExisteHerramientaParaFacturaSinAdjunto(t *testing.T) { } } } + +// 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) + } + } +}