From bae85033e0e6c54f8c15956b8731280cf967f820 Mon Sep 17 00:00:00 2001 From: Lucas Tettamanti Date: Mon, 18 May 2026 00:40:22 -0300 Subject: [PATCH] =?UTF-8?q?Agente:=20variar=20fallbacks=20+=20drop=20reply?= =?UTF-8?q?=5Ftemplates=20(c=C3=B3digo=20muerto)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los 5 fallbacks hardcoded en runTurn.js (cuando el LLM no llama tools, está paused, awaiting_human, o tira error) mandaban siempre el mismo string al cliente. Ahora hay un módulo fallbackReplies con 4 kinds × 3 variantes que rotan random — sin pasar por el LLM, sin estado, cero costo de latencia. Drop de la tabla reply_templates: tenía 45 variantes seedeadas en 13 slots pero ningún código la consultaba (grep -rn "reply_templates" en src/ y public/ → cero referencias). Era código zombi que iba contra el goal "conversacional, no menu-driven" del CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 --- .../20260518110000_drop_reply_templates.sql | 10 +++++ .../3-turn-engine/agent/fallbackReplies.js | 30 ++++++++++++++ .../agent/fallbackReplies.test.js | 40 +++++++++++++++++++ src/modules/3-turn-engine/agent/runTurn.js | 18 ++++----- 4 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 db/migrations/20260518110000_drop_reply_templates.sql create mode 100644 src/modules/3-turn-engine/agent/fallbackReplies.js create mode 100644 src/modules/3-turn-engine/agent/fallbackReplies.test.js diff --git a/db/migrations/20260518110000_drop_reply_templates.sql b/db/migrations/20260518110000_drop_reply_templates.sql new file mode 100644 index 0000000..ecfa6ee --- /dev/null +++ b/db/migrations/20260518110000_drop_reply_templates.sql @@ -0,0 +1,10 @@ +-- migrate:up +-- Eliminar tabla reply_templates: el código actual no la consulta (cero referencias +-- en src/ y public/). Las 45 filas seedeadas son código zombi. +DROP INDEX IF EXISTS idx_reply_active; +DROP TABLE IF EXISTS reply_templates; + +-- migrate:down +-- No recreamos. Para restaurar el schema + seed correr en orden: +-- 20260501110000_reply_templates.sql (schema + index) +-- 20260501130000_seed_piaf_settings_and_replies.sql (45 filas) diff --git a/src/modules/3-turn-engine/agent/fallbackReplies.js b/src/modules/3-turn-engine/agent/fallbackReplies.js new file mode 100644 index 0000000..ec1d580 --- /dev/null +++ b/src/modules/3-turn-engine/agent/fallbackReplies.js @@ -0,0 +1,30 @@ +const VARIANTS = { + no_understanding: [ + "Disculpame, no te entendí. ¿Me lo decís de otra forma?", + "Perdón, no te seguí. ¿Lo repetís?", + "Mmm, no me quedó claro. ¿Cómo era?", + ], + awaiting_human: [ + "Te paso con alguien que pueda ayudarte.", + "Dejame que te derive a un compañero, ya te responde.", + "Te conecto con una persona, esperá un toque.", + ], + paused: [ + "Dale, cuando quieras seguimos.", + "Bueno, te espero. Avisame cuando estés.", + "Listo, lo dejamos en pausa. Decime cuando seguimos.", + ], + llm_error: [ + "Disculpame, tuve un problema. ¿Lo intentás de nuevo?", + "Uy, se me trabó algo. ¿Lo escribís de nuevo?", + "Perdón, algo no anduvo bien. ¿Repetís?", + ], +}; + +export function pickFallbackReply(kind) { + const pool = VARIANTS[kind] || VARIANTS.no_understanding; + const idx = Math.floor(Math.random() * pool.length); + return pool[idx]; +} + +export const __FALLBACK_VARIANTS__ = VARIANTS; diff --git a/src/modules/3-turn-engine/agent/fallbackReplies.test.js b/src/modules/3-turn-engine/agent/fallbackReplies.test.js new file mode 100644 index 0000000..de2d75f --- /dev/null +++ b/src/modules/3-turn-engine/agent/fallbackReplies.test.js @@ -0,0 +1,40 @@ +import { pickFallbackReply, __FALLBACK_VARIANTS__ } from "./fallbackReplies.js"; + +const KINDS = ["no_understanding", "awaiting_human", "paused", "llm_error"]; + +describe("pickFallbackReply", () => { + it("cada kind tiene al menos 3 variantes", () => { + for (const k of KINDS) { + expect(__FALLBACK_VARIANTS__[k].length).toBeGreaterThanOrEqual(3); + } + }); + + it("retorna string no vacío para cada kind", () => { + for (const k of KINDS) { + const out = pickFallbackReply(k); + expect(typeof out).toBe("string"); + expect(out.length).toBeGreaterThan(0); + } + }); + + it("retorna una variante del pool correspondiente", () => { + for (const k of KINDS) { + const pool = __FALLBACK_VARIANTS__[k]; + const out = pickFallbackReply(k); + expect(pool).toContain(out); + } + }); + + it("kind inexistente cae a no_understanding (no rompe)", () => { + const out = pickFallbackReply("ese_kind_no_existe"); + expect(__FALLBACK_VARIANTS__.no_understanding).toContain(out); + }); + + it("variación: 100 llamadas cubren al menos 2 variantes distintas por kind", () => { + for (const k of KINDS) { + const seen = new Set(); + for (let i = 0; i < 100; i++) seen.add(pickFallbackReply(k)); + expect(seen.size).toBeGreaterThanOrEqual(2); + } + }); +}); diff --git a/src/modules/3-turn-engine/agent/runTurn.js b/src/modules/3-turn-engine/agent/runTurn.js index 260ef11..edbfb2b 100644 --- a/src/modules/3-turn-engine/agent/runTurn.js +++ b/src/modules/3-turn-engine/agent/runTurn.js @@ -16,6 +16,7 @@ import { SYSTEM_PROMPT } from "./systemPrompt.js"; import { TOOL_SCHEMAS } from "./tools/schemas.js"; import { executeToolCall } from "./tools/executor.js"; import { getCustomerProfile } from "./customerProfile.js"; +import { pickFallbackReply } from "./fallbackReplies.js"; import { debug as dbg } from "../../shared/debug.js"; const MAX_TOOL_CALLS = parseInt(process.env.AGENT_MAX_TOOL_CALLS || "10", 10); @@ -212,7 +213,7 @@ export async function runTurnAgent({ if (!calls.length) { // Sin tool calls → forzar say con fallback y salir audit.no_tool_calls = true; - ctx.say_text = ctx.say_text || msg.content || "Disculpame, no te entendí. ¿Me lo decís de otra forma?"; + ctx.say_text = ctx.say_text || msg.content || pickFallbackReply("no_understanding"); turnDone = true; break; } @@ -241,9 +242,13 @@ export async function runTurnAgent({ if (dbg.llm) console.error("[agent] error", llmError); } - // Si no hay say, fallback determinista + // Si no hay say, fallback con variante random if (!ctx.say_text) { - ctx.say_text = pickFallbackReply(ctx, llmError); + const kind = ctx.awaiting_human ? "awaiting_human" + : ctx.paused ? "paused" + : llmError ? "llm_error" + : "no_understanding"; + ctx.say_text = pickFallbackReply(kind); audit.fallback_used = true; } @@ -294,13 +299,6 @@ export async function runTurnAgent({ }; } -function pickFallbackReply(ctx, err) { - if (ctx.awaiting_human) return "Te paso con un humano que pueda ayudarte."; - if (ctx.paused) return "Dale, cuando quieras seguimos."; - if (err) return "Disculpame, tuve un problema. ¿Lo intentás de nuevo?"; - return "No te seguí, ¿me lo decís de otra forma?"; -} - function detectIntent(toolCalls = []) { const names = toolCalls.map((c) => c.name); if (names.includes("confirm_order")) return "confirm_order";