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:
10
db/migrations/20260518110000_drop_reply_templates.sql
Normal file
10
db/migrations/20260518110000_drop_reply_templates.sql
Normal 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)
|
||||||
30
src/modules/3-turn-engine/agent/fallbackReplies.js
Normal file
30
src/modules/3-turn-engine/agent/fallbackReplies.js
Normal 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;
|
||||||
40
src/modules/3-turn-engine/agent/fallbackReplies.test.js
Normal file
40
src/modules/3-turn-engine/agent/fallbackReplies.test.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@ import { SYSTEM_PROMPT } from "./systemPrompt.js";
|
|||||||
import { TOOL_SCHEMAS } from "./tools/schemas.js";
|
import { TOOL_SCHEMAS } from "./tools/schemas.js";
|
||||||
import { executeToolCall } from "./tools/executor.js";
|
import { executeToolCall } from "./tools/executor.js";
|
||||||
import { getCustomerProfile } from "./customerProfile.js";
|
import { getCustomerProfile } from "./customerProfile.js";
|
||||||
|
import { pickFallbackReply } from "./fallbackReplies.js";
|
||||||
import { debug as dbg } from "../../shared/debug.js";
|
import { debug as dbg } from "../../shared/debug.js";
|
||||||
|
|
||||||
const MAX_TOOL_CALLS = parseInt(process.env.AGENT_MAX_TOOL_CALLS || "10", 10);
|
const MAX_TOOL_CALLS = parseInt(process.env.AGENT_MAX_TOOL_CALLS || "10", 10);
|
||||||
@@ -212,7 +213,7 @@ export async function runTurnAgent({
|
|||||||
if (!calls.length) {
|
if (!calls.length) {
|
||||||
// Sin tool calls → forzar say con fallback y salir
|
// Sin tool calls → forzar say con fallback y salir
|
||||||
audit.no_tool_calls = true;
|
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;
|
turnDone = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -241,9 +242,13 @@ export async function runTurnAgent({
|
|||||||
if (dbg.llm) console.error("[agent] error", llmError);
|
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) {
|
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;
|
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 = []) {
|
function detectIntent(toolCalls = []) {
|
||||||
const names = toolCalls.map((c) => c.name);
|
const names = toolCalls.map((c) => c.name);
|
||||||
if (names.includes("confirm_order")) return "confirm_order";
|
if (names.includes("confirm_order")) return "confirm_order";
|
||||||
|
|||||||
Reference in New Issue
Block a user