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";