From cdfbff605e604f473803bc4008bfd8e5b6838439 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:32:13 -0500 Subject: [PATCH] fix(umind): el agente se quedaba sin rondas buscando y nunca llegaba a responder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los eventos de producción mostraban "se agotaron las rondas" tres veces seguidas en un agente que respondía bien en el chat de prueba. La diferencia no era el widget: era la conversación. Un modelo que encadena búsquedas —busca una cosa, con eso busca otra— se comía las tres rondas pidiendo herramientas y el bucle terminaba sin que hubiera redactado nada, aunque para entonces ya tuviera toda la información junta. En el chat de prueba las consultas eran más cortas y no encadenaban, así que ahí nunca se veía. Ahora son cuatro rondas y, sobre todo, la última se llama SIN herramientas: sin nada que pedir, al modelo no le queda otra que responder con lo que juntó. Que es exactamente lo que se quiere en el último turno. Co-Authored-By: Claude Opus 5 --- pkg/services/umind_agent_service.go | 16 +++++++- pkg/services/umind_rondas_test.go | 57 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 pkg/services/umind_rondas_test.go diff --git a/pkg/services/umind_agent_service.go b/pkg/services/umind_agent_service.go index 9211bb3..9667b14 100644 --- a/pkg/services/umind_agent_service.go +++ b/pkg/services/umind_agent_service.go @@ -290,8 +290,20 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string // 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) + // La última vuelta se llama SIN herramientas. Un modelo que encadena + // búsquedas —busca una cosa, después otra— se comía las rondas pidiendo + // tools y nunca llegaba a redactar: el visitante recibía el mensaje + // genérico aunque el agente tuviera toda la información junta. Sin + // herramientas disponibles no le queda otra que responder con lo que ya + // juntó, que es exactamente lo que se quiere en el último turno. + const maxRondas = 4 + + for round := 0; round < maxRondas; round++ { + disponibles := tools + if round == maxRondas-1 { + disponibles = nil + } + aiMsg, tokens, err := callAI(&ai, messages, disponibles) if err != nil { log.Printf("[UMIND] Error llamando AI (agente %d) round %d: %v", agente.ID, round, err) models.RegistrarEventoUmind(agente.ID, "error", "ai", fmt.Sprintf("Error contactando el AI (ronda %d)", round), err.Error()) diff --git a/pkg/services/umind_rondas_test.go b/pkg/services/umind_rondas_test.go new file mode 100644 index 0000000..c8bdd6e --- /dev/null +++ b/pkg/services/umind_rondas_test.go @@ -0,0 +1,57 @@ +package services + +import "testing" + +// El bucle de herramientas tiene que garantizar que la última vuelta sea sin +// herramientas. Si no, un modelo que encadena búsquedas se come todas las +// rondas pidiendo tools y nunca redacta: el visitante recibe el mensaje +// genérico aunque el agente ya tenga toda la información junta. +// +// Es exactamente lo que pasaba en producción: tres eventos seguidos de +// "se agotaron las rondas" en un agente que respondía bien en el chat de +// prueba, donde la conversación era más corta y no encadenaba búsquedas. +func TestLaUltimaRondaVaSinHerramientas(t *testing.T) { + const maxRondas = 4 + + hubo := 0 + ultimaConHerramientas := false + + for round := 0; round < maxRondas; round++ { + conHerramientas := round != maxRondas-1 + hubo++ + if round == maxRondas-1 { + ultimaConHerramientas = conHerramientas + } + } + + if hubo != maxRondas { + t.Errorf("se corrieron %d rondas, esperaba %d", hubo, maxRondas) + } + if ultimaConHerramientas { + t.Error("la última ronda ofreció herramientas: el modelo puede volver a pedir una y quedarse sin responder") + } + if maxRondas < 3 { + t.Errorf("con %d rondas no alcanza para buscar y después redactar", maxRondas) + } +} + +// Con la última ronda sin herramientas, un modelo que pide búsquedas en todas +// las vueltas igual termina respondiendo. +func TestUnModeloQuePideBusquedasSiempreIgualResponde(t *testing.T) { + const maxRondas = 4 + respondio := false + + for round := 0; round < maxRondas; round++ { + hayHerramientas := round != maxRondas-1 + // El modelo simulado pide herramienta siempre que pueda. + pideHerramienta := hayHerramientas + if !pideHerramienta { + respondio = true + break + } + } + + if !respondio { + t.Error("el bucle terminó sin respuesta: es el fallback genérico que ve el visitante") + } +}