Files
botino/src/modules/3-turn-engine/nlu/index.js
Lucas Tettamanti 5e79f17d00 20260204
2026-02-04 16:06:51 -03:00

196 lines
6.2 KiB
JavaScript

/**
* NLU Modular - Punto de entrada principal
*
* Orquesta el Router + Specialists para procesar mensajes de usuario.
* Reemplaza a llmNluV3 con una arquitectura modular y prompts editables.
*/
import { routerClassify, quickDomainDetect } from "./router.js";
import { greetingNlu } from "./specialists/greeting.js";
import { ordersNlu } from "./specialists/orders.js";
import { shippingNlu } from "./specialists/shipping.js";
import { paymentNlu } from "./specialists/payment.js";
import { browseNlu } from "./specialists/browse.js";
import { createEmptyNlu } from "./schemas.js";
// Re-exportar utilidades útiles
export { loadPrompt, invalidatePromptCache, AVAILABLE_VARIABLES } from "./promptLoader.js";
export { PROMPT_KEYS, DEFAULT_MODELS } from "../../0-ui/db/promptsRepo.js";
/**
* Procesa un mensaje con el sistema NLU modular
*
* @param {Object} params
* @param {Object} params.input - Input del NLU
* @param {string} params.input.last_user_message - Mensaje del usuario
* @param {string} params.input.conversation_state - Estado actual de la conversación
* @param {Object} params.input.pending_context - Contexto de items pendientes
* @param {string} params.input.locale - Locale (default: es-AR)
* @param {number} params.tenantId - ID del tenant
* @param {Object} params.storeConfig - Configuración de la tienda (para variables)
* @returns {Object} { nlu, raw_text, model, usage, schema, validation, routing }
*/
export async function llmNluModular({ input, tenantId, storeConfig = {} } = {}) {
const text = input?.last_user_message || "";
const state = input?.conversation_state || "IDLE";
const startTime = Date.now();
// Tracking para debug
const routing = {
quick_detect: null,
router_result: null,
final_domain: null,
specialist_used: null,
};
try {
// 1) Quick detection: si es un caso obvio, evitar llamar al router LLM
const quickDomain = quickDomainDetect(text, state);
routing.quick_detect = quickDomain;
// Casos donde podemos saltar el router:
// - Saludos simples
// - Números solos (1, 2) en estados SHIPPING/PAYMENT
// - Patrones muy claros
const skipRouter = shouldSkipRouter(text, state, quickDomain);
let domain;
if (skipRouter) {
domain = quickDomain;
routing.router_result = { skipped: true, quick_domain: quickDomain };
} else {
// 2) Router LLM: clasificar dominio
const routerResult = await routerClassify({ tenantId, text, state, storeConfig });
domain = routerResult.domain;
routing.router_result = routerResult;
}
routing.final_domain = domain;
// 3) Dispatch al specialist correspondiente
let result;
switch (domain) {
case "greeting":
routing.specialist_used = "greeting";
result = await greetingNlu({ tenantId, text, storeConfig });
break;
case "orders":
routing.specialist_used = "orders";
result = await ordersNlu({ tenantId, text, storeConfig });
break;
case "shipping":
routing.specialist_used = "shipping";
result = await shippingNlu({ tenantId, text, storeConfig });
break;
case "payment":
routing.specialist_used = "payment";
result = await paymentNlu({ tenantId, text, storeConfig });
break;
case "browse":
routing.specialist_used = "browse";
result = await browseNlu({ tenantId, text, storeConfig });
break;
default:
// Fallback: usar orders como default si hay texto con posibles productos
routing.specialist_used = "orders_fallback";
result = await ordersNlu({ tenantId, text, storeConfig });
// Pero marcar como "other" si el resultado no es claro
if (result.nlu.confidence < 0.7) {
result.nlu.intent = "other";
}
}
// Agregar metadata de routing
result.routing = routing;
result.schema = "modular_v1";
result.processing_time_ms = Date.now() - startTime;
return result;
} catch (error) {
console.error("[nluModular] Error:", error);
// Fallback completo
const nlu = createEmptyNlu();
nlu.intent = "other";
nlu.confidence = 0;
return {
nlu,
raw_text: "",
model: null,
usage: null,
schema: "modular_v1",
validation: { ok: false, error: error.message },
routing: { ...routing, error: error.message },
processing_time_ms: Date.now() - startTime,
};
}
}
/**
* Determina si podemos saltar el router LLM y usar quick detection
*/
function shouldSkipRouter(text, state, quickDomain) {
const t = String(text || "").trim();
// Saludos simples (sin productos)
if (quickDomain === "greeting" && t.length < 20) {
return true;
}
// Números solos en estados específicos
if (/^[12]$/.test(t)) {
if (state === "SHIPPING" || state === "PAYMENT") {
return true;
}
}
// "efectivo" o "tarjeta" solos en estado PAYMENT
if (state === "PAYMENT" && /^(efectivo|tarjeta|link|transfer)$/i.test(t)) {
return true;
}
// "delivery" o "retiro" solos en estado SHIPPING
if (state === "SHIPPING" && /^(delivery|retiro|buscar|sucursal)$/i.test(t)) {
return true;
}
// En estado SHIPPING, si quickDomain ya detectó "shipping" (dirección), confiar en eso
// Esto evita que el router LLM clasifique direcciones como productos
if (state === "SHIPPING" && quickDomain === "shipping") {
return true;
}
return false;
}
/**
* Versión compatible con la firma de llmNluV3
* Para usar con el feature flag sin cambiar mucho código
*/
export async function llmNluModularCompat({ input, model } = {}) {
// Extraer tenantId del input si está disponible, o usar 1 como default
// En producción, esto debería pasarse explícitamente
const tenantId = input?.tenantId || 1;
// Construir storeConfig básico (en producción se cargaría de la DB)
const storeConfig = {
name: input?.store_name || "la carnicería",
botName: input?.bot_name || "Piaf",
hours: input?.store_hours || "",
address: input?.store_address || "",
};
return llmNluModular({ input, tenantId, storeConfig });
}
// Export default para compatibilidad
export default llmNluModular;