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:
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 { 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";
|
||||
|
||||
Reference in New Issue
Block a user