/** * 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;