modularizado de prompts

This commit is contained in:
Lucas Tettamanti
2026-01-25 20:51:33 -03:00
parent b91ece867b
commit a489ec66a2
43 changed files with 5408 additions and 89 deletions

View File

@@ -0,0 +1,175 @@
/**
* Router NLU - Clasifica el dominio del mensaje
*
* Usa un prompt ligero para clasificar rápidamente el tipo de mensaje
* antes de enviarlo al specialist correspondiente.
*/
import OpenAI from "openai";
import { loadPrompt } from "./promptLoader.js";
import { validateRouter, getValidationErrors } from "./schemas.js";
let _client = null;
function getClient() {
const apiKey = process.env.OPENAI_API_KEY || process.env.OPENAI_APIKEY;
if (!apiKey) {
throw new Error("OPENAI_API_KEY is not set");
}
if (!_client) {
_client = new OpenAI({ apiKey });
}
return _client;
}
/**
* Extrae JSON de una respuesta de texto
*/
function extractJson(text) {
const s = String(text || "");
const i = s.indexOf("{");
const j = s.lastIndexOf("}");
if (i >= 0 && j > i) {
try {
return JSON.parse(s.slice(i, j + 1));
} catch {
return null;
}
}
return null;
}
/**
* Clasifica el dominio del mensaje
*
* @param {Object} params
* @param {number} params.tenantId - ID del tenant
* @param {string} params.text - Mensaje del usuario
* @param {string} params.state - Estado actual de la conversación
* @param {Object} params.storeConfig - Config de la tienda (para variables)
* @returns {Object} { domain: string, raw_text: string, model: string }
*/
export async function routerClassify({ tenantId, text, state, storeConfig = {} }) {
const openai = getClient();
// Cargar prompt del router
const { content: systemPrompt, model } = await loadPrompt({
tenantId,
promptKey: "router",
variables: {
state: state || "IDLE",
...storeConfig,
},
});
// Hacer la llamada al LLM
const response = await openai.chat.completions.create({
model: model || "gpt-4o-mini",
temperature: 0.1,
max_tokens: 50,
response_format: { type: "json_object" },
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: text },
],
});
const rawText = response?.choices?.[0]?.message?.content || "";
let parsed = extractJson(rawText);
// Validar respuesta
if (!parsed || !validateRouter(parsed)) {
// Fallback: intentar detectar por patrones simples
parsed = { domain: detectDomainByPatterns(text, state) };
}
return {
domain: parsed.domain || "other",
raw_text: rawText,
model: model,
usage: response?.usage || null,
};
}
/**
* Detección de dominio por patrones (fallback)
*/
function detectDomainByPatterns(text, state) {
const t = String(text || "").toLowerCase().trim();
// Greeting patterns (solo si no menciona productos)
const greetingPatterns = /^(hola|buenas?|buen d[ií]a|buenas tardes|buenas noches|qu[eé] tal|hey|hi|holis)\s*[!?.,]*$/i;
if (greetingPatterns.test(t)) {
return "greeting";
}
// Si el estado ya es SHIPPING o PAYMENT, priorizar esos dominios
if (state === "SHIPPING") {
if (/delivery|env[ií]o|retiro|buscar|sucursal|direcci[oó]n/i.test(t)) {
return "shipping";
}
// Si parece una dirección (tiene números y palabras)
if (/\d+/.test(t) && /[a-záéíóú]{3,}/i.test(t)) {
return "shipping";
}
}
if (state === "PAYMENT") {
if (/efectivo|cash|tarjeta|link|transfer|mercadopago|mp|qr/i.test(t)) {
return "payment";
}
// Números simples (1 o 2) en estado PAYMENT
if (/^[12]$/.test(t.trim())) {
return "payment";
}
}
// Orders patterns
const orderPatterns = [
/\b(quiero|dame|anotame|poneme|agregame|necesito)\b/i,
/\b(sacame|quitame|eliminame)\b/i,
/\b(qu[eé] tengo|qu[eé] anot[eé]|mi pedido|ver carrito)\b/i,
/\b(listo|eso es todo|cerrar|confirmar)\b/i,
/\d+\s*(kg|kilo|gramo|g|unidad)/i, // cantidad + unidad
];
if (orderPatterns.some(p => p.test(t))) {
return "orders";
}
// Browse patterns
const browsePatterns = [
/\b(cu[aá]nto (sale|cuesta|est[aá]))\b/i,
/\b(precio de|precios)\b/i,
/\b(ten[eé]s|hay|vend[eé]s|tienen)\b/i,
/\b(qu[eé] me recomend[aá]s|recomendaci[oó]n)\b/i,
/\bpara\s+\d+\s*(personas?|comensales?)\b/i,
];
if (browsePatterns.some(p => p.test(t))) {
return "browse";
}
// Shipping patterns
if (/\b(delivery|env[ií]o|retiro|buscar|sucursal)\b/i.test(t)) {
return "shipping";
}
// Payment patterns
if (/\b(efectivo|tarjeta|link|transfer|mercadopago)\b/i.test(t)) {
return "payment";
}
// Default basado en estado
if (state === "CART") return "orders";
if (state === "SHIPPING") return "shipping";
if (state === "PAYMENT") return "payment";
return "other";
}
/**
* Detecta dominio solo por patrones (sin LLM)
* Útil para casos obvios o cuando queremos ahorrar latencia
*/
export function quickDomainDetect(text, state) {
return detectDomainByPatterns(text, state);
}