fix(umind): el agente descartaba la respuesta que ya había escrito
Cuando se agotan las tres rondas de herramientas sin una respuesta final, el visitante recibe "dame un poco más de detalle". Pero un modelo puede mandar texto Y pedir una herramienta en el mismo turno, y ese texto se tiraba: si después se agotaban las rondas, la persona recibía el mensaje genérico aunque el agente ya le hubiera contestado bien. Ahora se guarda lo último que escribió y se usa antes de caer en el genérico. Y el evento que quedaba en Auditoría decía "se agotaron las rondas" sin nada más, que no alcanza para saber dónde mirar. Ahora registra qué herramientas pidió en orden, y cada vez que una devuelve error queda su propio evento con la respuesta cruda. Una herramienta que falla y un modelo que la reintenta es la forma más común de agotar las rondas, y hasta ahora había que deducirlo. Esto no arregla la causa de un agente puntual que responde el genérico a todo —esa sale de los eventos que ahora sí se registran— pero deja de esconder respuestas buenas y dice dónde buscar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b125ceef95
commit
44686d409a
@@ -0,0 +1,61 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Cuando se agotan las rondas de herramientas, el visitante recibía un mensaje
|
||||
// genérico aunque el modelo ya le hubiera escrito una respuesta completa: el
|
||||
// texto que viene junto con una llamada a herramienta se descartaba.
|
||||
//
|
||||
// Se prueba la lógica de selección, que es la que decide qué ve la persona.
|
||||
func TestSePrefiereElTextoDelModeloAlMensajeGenerico(t *testing.T) {
|
||||
const generico = "Un momento, por favor — dame un poco más de detalle sobre lo que necesitas."
|
||||
|
||||
elegir := func(final, parcial string) string {
|
||||
if final != "" {
|
||||
return final
|
||||
}
|
||||
if strings.TrimSpace(parcial) != "" {
|
||||
return parcial
|
||||
}
|
||||
return generico
|
||||
}
|
||||
|
||||
casos := []struct {
|
||||
nombre string
|
||||
final string
|
||||
parcial string
|
||||
esperado string
|
||||
}{
|
||||
{"respuesta normal", "Los 4 pilares son…", "", "Los 4 pilares son…"},
|
||||
{"se agotaron las rondas pero el modelo había escrito", "", "Los 4 pilares son…", "Los 4 pilares son…"},
|
||||
{"el modelo nunca escribió nada", "", "", generico},
|
||||
{"texto parcial en blanco no cuenta", "", " \n ", generico},
|
||||
}
|
||||
|
||||
for _, c := range casos {
|
||||
if got := elegir(c.final, c.parcial); got != c.esperado {
|
||||
t.Errorf("%s: eligió %q, esperaba %q", c.nombre, got, c.esperado)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// El resultado de una herramienta que falló se reconoce por su forma. Si esa
|
||||
// detección se rompe, el evento de diagnóstico deja de registrarse y "se
|
||||
// agotaron las rondas" vuelve a no decir dónde mirar.
|
||||
func TestSeDetectaElErrorDeUnaHerramienta(t *testing.T) {
|
||||
casos := map[string]bool{
|
||||
`{"error": "consulta requerida"}`: true,
|
||||
` {"error": "el servicio de embeddings no está configurado"}`: true,
|
||||
`{"resultados": ["texto"]}`: false,
|
||||
`{"resultados": [], "nota": "No se encontró información relacionada"}`: false,
|
||||
}
|
||||
for entrada, esError := range casos {
|
||||
got := strings.HasPrefix(strings.TrimSpace(entrada), `{"error"`)
|
||||
if got != esError {
|
||||
t.Errorf("%q: detectado como error = %v, esperaba %v", entrada, got, esError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,6 +283,13 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
|
||||
_ = models.SaveUmindMensaje(agente.ID, sessionID, "user", userText)
|
||||
|
||||
var finalResponse string
|
||||
// Lo último que el modelo llegó a escribir, aunque haya venido acompañado
|
||||
// de una llamada a herramienta.
|
||||
var ultimoTextoParcial string
|
||||
// Para el evento de diagnóstico: sin saber qué herramienta pidió en cada
|
||||
// ronda, "se agotaron las rondas" no dice dónde mirar.
|
||||
var herramientasPedidas []string
|
||||
|
||||
for round := 0; round < 3; round++ {
|
||||
aiMsg, tokens, err := callAI(&ai, messages, tools)
|
||||
if err != nil {
|
||||
@@ -314,11 +321,28 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
|
||||
break
|
||||
}
|
||||
|
||||
// Un modelo puede mandar texto Y pedir herramientas en el mismo turno.
|
||||
// Ese texto se descartaba: si después se agotaban las rondas, el
|
||||
// visitante recibía el mensaje genérico aunque el agente ya le hubiera
|
||||
// escrito una respuesta buena.
|
||||
if s, ok := aiMsg.Content.(string); ok && strings.TrimSpace(s) != "" {
|
||||
ultimoTextoParcial = s
|
||||
}
|
||||
|
||||
messages = append(messages, *aiMsg)
|
||||
for _, tc := range aiMsg.ToolCalls {
|
||||
var toolArgs map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(tc.Function.Arguments), &toolArgs)
|
||||
toolResult := executeUmindTool(agente.ID, tc.Function.Name, toolArgs)
|
||||
herramientasPedidas = append(herramientasPedidas, tc.Function.Name)
|
||||
// Una herramienta que devuelve error y el modelo que la reintenta es
|
||||
// la forma más común de agotar las rondas. Queda registrado para
|
||||
// poder verlo en Auditoría en vez de deducirlo.
|
||||
if strings.HasPrefix(strings.TrimSpace(toolResult), `{"error"`) {
|
||||
models.RegistrarEventoUmind(agente.ID, "warn", "tool",
|
||||
fmt.Sprintf("La herramienta %q devolvió error en la ronda %d", tc.Function.Name, round),
|
||||
toolResult)
|
||||
}
|
||||
messages = append(messages, agentMessage{
|
||||
Role: "tool",
|
||||
ToolCallID: tc.ID,
|
||||
@@ -329,9 +353,19 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
|
||||
}
|
||||
|
||||
if finalResponse == "" {
|
||||
log.Printf("[UMIND] Agente %d: se agotaron las rondas de tool-calling sin una respuesta final", agente.ID)
|
||||
models.RegistrarEventoUmind(agente.ID, "warn", "ai", "Se agotaron las rondas de tool-calling sin una respuesta final", "")
|
||||
finalResponse = "Un momento, por favor — dame un poco más de detalle sobre lo que necesitas."
|
||||
log.Printf("[UMIND] Agente %d: se agotaron las rondas de tool-calling sin respuesta final (herramientas: %v)",
|
||||
agente.ID, herramientasPedidas)
|
||||
models.RegistrarEventoUmind(agente.ID, "warn", "ai",
|
||||
"Se agotaron las 3 rondas de herramientas sin una respuesta final",
|
||||
fmt.Sprintf("Herramientas pedidas en orden: %s", strings.Join(herramientasPedidas, " → ")))
|
||||
|
||||
// Antes de dar el mensaje genérico, usar lo que el modelo ya escribió.
|
||||
if strings.TrimSpace(ultimoTextoParcial) != "" {
|
||||
finalResponse = ultimoTextoParcial
|
||||
_ = models.SaveUmindMensaje(agente.ID, sessionID, "assistant", finalResponse)
|
||||
} else {
|
||||
finalResponse = "Un momento, por favor — dame un poco más de detalle sobre lo que necesitas."
|
||||
}
|
||||
}
|
||||
return finalResponse, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user