El bot conversacional no maneja pagos. Su trabajo: pedidos, datos de entrega, dejar la orden anotada en Woo (status=pending). El cobro lo gestiona el comercio offline. Todo lo de payment_type / is_paid / PAYMENT / WAITING_WEBHOOKS era legacy de un flow viejo que se baja. Nuevo flow: IDLE → CART → SHIPPING → IDLE (con orden creada). Cuando el usuario completa shipping (pickup elegido OR delivery+address), shipping.js emite create_order y el bot cierra con order.confirmed. - fsm.js: 4 estados (IDLE/CART/SHIPPING/AWAITING_HUMAN). hasPaymentInfo e isPaid eliminados. deriveNextState gira SHIPPING→IDLE en vez de →PAYMENT→WAITING. ALLOWED transitions actualizadas. - orderModel.js: createEmptyOrder() sin payment_type/is_paid. migrateOldContext deja de leer payment_method / mp.payment_status. - stateHandlers: payment.js y waiting.js eliminados. shipping.js gana finalizeOrder() que emite create_order action y vuelve a IDLE. - replyTemplates: payment.* y waiting.* fuera. order.confirmed nuevo, con 3 variantes y rewriter habilitado. - NLU openai.js + nlu/schemas.js: select_payment fuera del enum, payment_method fuera de entities. Prompt sin la regla de SELECCIONAR PAGO. - nlu/router.js + nlu/index.js: dominio "payment" eliminado. shouldSkipRouter ya no chequea PAYMENT. - nlu/specialists/payment.js: eliminado. - promptsRepo.js + promptLoader.js: PROMPT_KEYS sin "payment". - turnEngineV3.js: switch ya no dispatcha a PAYMENT/WAITING. normalizeState mapea estados legacy (PAYMENT/WAITING_WEBHOOKS/COMPLETED) a IDLE. context_patch ya no emite payment_method. - wooOrders.createOrder: paymentMethod param eliminado. Order queda en status=pending sin payment_method (cobro offline). - pipeline.js: paymentMethod fuera del create_order glue. Invariant "no_checkout_without_payment_link" eliminado. signal payment_selected reemplazado por shipping_completed. - XState machine: top-level PAYMENT y WAITING eliminados. SELECT_PAYMENT event fuera. SHIPPING ahora cierra con enqueueWooCreateOrder + replyOrderConfirmed → IDLE. Guards hasPayment/isPaid borrados. - Tests fsm.test.js / orderModel.test.js / machine/index.test.js actualizados al nuevo contrato. 188 tests pasando. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
5.8 KiB
JavaScript
183 lines
5.8 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 { 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 estado SHIPPING
|
|
// - 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 "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 estado SHIPPING (selección 1/2)
|
|
if (/^[12]$/.test(t) && state === "SHIPPING") {
|
|
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;
|