Agente: variar fallbacks + drop reply_templates (código muerto)

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 <noreply@anthropic.com>
This commit is contained in:
Lucas Tettamanti
2026-05-18 00:40:22 -03:00
parent 423a99ab48
commit bae85033e0
4 changed files with 88 additions and 10 deletions

View File

@@ -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)

View File

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

View File

@@ -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);
}
});
});

View File

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