modularizado de prompts
This commit is contained in:
162
src/modules/3-turn-engine/nlu/specialists/orders.js
Normal file
162
src/modules/3-turn-engine/nlu/specialists/orders.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Orders Specialist - Extracción de productos y cantidades
|
||||
*
|
||||
* El specialist más importante: maneja add_to_cart, remove_from_cart,
|
||||
* view_cart, confirm_order con soporte para multi-items.
|
||||
*/
|
||||
|
||||
import OpenAI from "openai";
|
||||
import { loadPrompt } from "../promptLoader.js";
|
||||
import { validateOrders, getValidationErrors, createEmptyNlu } 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza unidades a formato estándar
|
||||
*/
|
||||
function normalizeUnit(unit) {
|
||||
if (!unit) return null;
|
||||
const u = String(unit).toLowerCase().trim();
|
||||
if (["kg", "kilo", "kilos", "kilogramo", "kilogramos"].includes(u)) return "kg";
|
||||
if (["g", "gr", "gramo", "gramos"].includes(u)) return "g";
|
||||
if (["unidad", "unidades", "u", "un"].includes(u)) return "unidad";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza items extraídos
|
||||
*/
|
||||
function normalizeItems(items) {
|
||||
if (!Array.isArray(items) || items.length === 0) return null;
|
||||
|
||||
return items
|
||||
.filter(item => item && item.product_query)
|
||||
.map(item => ({
|
||||
product_query: String(item.product_query || "").trim(),
|
||||
quantity: typeof item.quantity === "number" ? item.quantity : null,
|
||||
unit: normalizeUnit(item.unit),
|
||||
}))
|
||||
.filter(item => item.product_query.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa un mensaje de pedido
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {number} params.tenantId - ID del tenant
|
||||
* @param {string} params.text - Mensaje del usuario
|
||||
* @param {Object} params.storeConfig - Config de la tienda
|
||||
* @returns {Object} NLU unificado
|
||||
*/
|
||||
export async function ordersNlu({ tenantId, text, storeConfig = {} }) {
|
||||
const openai = getClient();
|
||||
|
||||
// Cargar prompt de orders
|
||||
const { content: systemPrompt, model } = await loadPrompt({
|
||||
tenantId,
|
||||
promptKey: "orders",
|
||||
variables: storeConfig,
|
||||
});
|
||||
|
||||
// Hacer la llamada al LLM
|
||||
const response = await openai.chat.completions.create({
|
||||
model: model || "gpt-4-turbo",
|
||||
temperature: 0.1, // Baja temperatura para extracción precisa
|
||||
max_tokens: 500,
|
||||
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);
|
||||
|
||||
// Intentar validar
|
||||
let validationOk = false;
|
||||
if (parsed && validateOrders(parsed)) {
|
||||
validationOk = true;
|
||||
} else if (parsed) {
|
||||
// Intentar normalizar respuesta parcialmente válida
|
||||
parsed = {
|
||||
intent: parsed.intent || "add_to_cart",
|
||||
confidence: parsed.confidence || 0.8,
|
||||
items: parsed.items || null,
|
||||
product_query: parsed.product_query || null,
|
||||
quantity: parsed.quantity || null,
|
||||
unit: parsed.unit || null,
|
||||
};
|
||||
validationOk = true;
|
||||
} else {
|
||||
// Fallback total
|
||||
parsed = {
|
||||
intent: "add_to_cart",
|
||||
confidence: 0.5,
|
||||
items: null,
|
||||
product_query: text.length < 50 ? text : null,
|
||||
quantity: null,
|
||||
unit: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Normalizar items - SIEMPRE convertir a array
|
||||
let normalizedItems = normalizeItems(parsed.items);
|
||||
|
||||
// Si no hay items pero hay product_query en raíz, convertir a array
|
||||
if ((!normalizedItems || normalizedItems.length === 0) && parsed.product_query) {
|
||||
normalizedItems = [{
|
||||
product_query: String(parsed.product_query).trim(),
|
||||
quantity: typeof parsed.quantity === "number" ? parsed.quantity : null,
|
||||
unit: normalizeUnit(parsed.unit),
|
||||
}];
|
||||
}
|
||||
|
||||
// Convertir a formato NLU unificado
|
||||
const nlu = createEmptyNlu();
|
||||
nlu.intent = parsed.intent || "add_to_cart";
|
||||
nlu.confidence = parsed.confidence || 0.8;
|
||||
|
||||
// Entities - siempre usar items[], nunca campos individuales
|
||||
nlu.entities.items = normalizedItems || [];
|
||||
nlu.entities.product_query = null; // Deprecado, usar items[]
|
||||
nlu.entities.quantity = null;
|
||||
nlu.entities.unit = null;
|
||||
|
||||
// Needs
|
||||
nlu.needs.catalog_lookup = ["add_to_cart", "remove_from_cart"].includes(nlu.intent);
|
||||
nlu.needs.knowledge_lookup = false;
|
||||
|
||||
return {
|
||||
nlu,
|
||||
raw_text: rawText,
|
||||
model,
|
||||
usage: response?.usage || null,
|
||||
validation: { ok: validationOk, errors: validationOk ? [] : getValidationErrors(validateOrders) },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user