modificando el patron del sistema, orientado mas al usuario
This commit is contained in:
@@ -53,7 +53,9 @@ export function createPendingItem({
|
||||
selected_unit = null,
|
||||
qty = null,
|
||||
unit = null,
|
||||
status = PendingStatus.NEEDS_TYPE
|
||||
status = PendingStatus.NEEDS_TYPE,
|
||||
requested_qty = null,
|
||||
requested_unit = null,
|
||||
}) {
|
||||
return {
|
||||
id: id || `pending_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
@@ -66,6 +68,8 @@ export function createPendingItem({
|
||||
qty, // Cantidad (null si NEEDS_QUANTITY)
|
||||
unit, // Unidad elegida por usuario
|
||||
status,
|
||||
requested_qty, // Cantidad pedida originalmente por el usuario (para usar después de selección)
|
||||
requested_unit, // Unidad pedida originalmente por el usuario
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "./orderModel.js";
|
||||
import { handleRecommend } from "./recommendations.js";
|
||||
import { getProductQtyRules } from "../0-ui/db/repo.js";
|
||||
import { createHumanTakeoverResponse } from "./nlu/humanFallback.js";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Utilidades
|
||||
@@ -134,9 +135,55 @@ export async function handleCartState({ tenantId, text, nlu, order, audit, fromI
|
||||
let currentOrder = order || createEmptyOrder();
|
||||
const actions = [];
|
||||
|
||||
// 1) Si hay pending items sin resolver, procesar clarificación
|
||||
// Intents que tienen prioridad sobre pending items (permiten "escapar" del loop)
|
||||
const priorityIntents = ["view_cart", "confirm_order", "greeting"];
|
||||
const isPriorityIntent = priorityIntents.includes(intent);
|
||||
|
||||
// Detectar si el usuario quiere cancelar/saltar el pending item actual
|
||||
const pendingItem = getNextPendingItem(currentOrder);
|
||||
if (pendingItem) {
|
||||
const cancelPhrases = /\b(dejalo|dejá|deja|olvidalo|olvida|nada|no importa|saltea|saltar|otro|siguiente|cancelar|skip)\b/i;
|
||||
const wantsToSkipPending = pendingItem && cancelPhrases.test(text || "");
|
||||
|
||||
// Si quiere saltar el pending, eliminarlo
|
||||
if (wantsToSkipPending && pendingItem) {
|
||||
currentOrder = {
|
||||
...currentOrder,
|
||||
pending: (currentOrder.pending || []).filter(p => p.id !== pendingItem.id),
|
||||
};
|
||||
audit.skipped_pending = pendingItem.query;
|
||||
|
||||
// Si hay más pending items, continuar con el siguiente
|
||||
const nextPending = getNextPendingItem(currentOrder);
|
||||
if (nextPending) {
|
||||
const { question } = formatOptionsForDisplay(nextPending);
|
||||
return {
|
||||
plan: {
|
||||
reply: `Ok, salteo "${pendingItem.query}". ${question}`,
|
||||
next_state: ConversationState.CART,
|
||||
intent: "add_to_cart",
|
||||
missing_fields: ["product_selection"],
|
||||
order_action: "none",
|
||||
},
|
||||
decision: { actions: [], order: currentOrder, audit },
|
||||
};
|
||||
}
|
||||
|
||||
// No hay más pending, mostrar confirmación
|
||||
const cartDisplay = formatCartForDisplay(currentOrder);
|
||||
return {
|
||||
plan: {
|
||||
reply: `Ok, salteo "${pendingItem.query}".\n\n${cartDisplay}\n\n¿Algo más?`,
|
||||
next_state: ConversationState.CART,
|
||||
intent: "other",
|
||||
missing_fields: [],
|
||||
order_action: "none",
|
||||
},
|
||||
decision: { actions: [], order: currentOrder, audit },
|
||||
};
|
||||
}
|
||||
|
||||
// 1) Si hay pending items sin resolver Y NO es un intent prioritario, procesar clarificación
|
||||
if (pendingItem && !isPriorityIntent) {
|
||||
const result = await processPendingClarification({ tenantId, text, nlu, order: currentOrder, pendingItem, audit });
|
||||
if (result) return result;
|
||||
}
|
||||
@@ -768,6 +815,8 @@ function createPendingItemFromSearch({ query, quantity, unit, candidates }) {
|
||||
}
|
||||
|
||||
// Multiple candidates, needs selection
|
||||
// Guardar la cantidad y unidad que pidió el usuario para usarla después de seleccionar
|
||||
const hasQty = quantity != null && Number.isFinite(Number(quantity)) && Number(quantity) > 0;
|
||||
return createPendingItem({
|
||||
query,
|
||||
candidates: cands.slice(0, 20).map(c => ({
|
||||
@@ -778,6 +827,9 @@ function createPendingItemFromSearch({ query, quantity, unit, candidates }) {
|
||||
display_unit: normalizeUnit(c.sell_unit) || inferDefaultUnit({ name: c.name, categories: c.categories }),
|
||||
})),
|
||||
status: PendingStatus.NEEDS_TYPE,
|
||||
// Guardar cantidad/unidad pedida para usarla al seleccionar
|
||||
requested_qty: hasQty ? Number(quantity) : null,
|
||||
requested_unit: normalizeUnit(unit) || null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -806,7 +858,19 @@ async function processPendingClarification({ tenantId, text, nlu, order, pending
|
||||
if (idx && pendingItem.candidates && idx <= pendingItem.candidates.length) {
|
||||
const selected = pendingItem.candidates[idx - 1];
|
||||
const displayUnit = selected.display_unit || inferDefaultUnit({ name: selected.name, categories: [] });
|
||||
const needsQuantity = displayUnit !== "unit";
|
||||
|
||||
// Usar la cantidad que pidió originalmente el usuario, o la unidad pedida para determinar si necesita cantidad
|
||||
const requestedQty = pendingItem.requested_qty;
|
||||
const requestedUnit = pendingItem.requested_unit || displayUnit;
|
||||
const hasRequestedQty = requestedQty != null && Number.isFinite(requestedQty) && requestedQty > 0;
|
||||
|
||||
// Si vende por peso y no tenemos cantidad, preguntar. Si vende por unidad y tiene cantidad, usar esa.
|
||||
const sellsByWeight = displayUnit !== "unit";
|
||||
const needsQuantity = sellsByWeight && !hasRequestedQty;
|
||||
|
||||
// Determinar la cantidad final
|
||||
const finalQty = hasRequestedQty ? requestedQty : 1;
|
||||
const finalUnit = requestedUnit || displayUnit;
|
||||
|
||||
const updatedOrder = updatePendingItem(order, pendingItem.id, {
|
||||
selected_woo_id: selected.woo_id,
|
||||
@@ -815,8 +879,8 @@ async function processPendingClarification({ tenantId, text, nlu, order, pending
|
||||
selected_unit: displayUnit,
|
||||
candidates: [],
|
||||
status: needsQuantity ? PendingStatus.NEEDS_QUANTITY : PendingStatus.READY,
|
||||
qty: needsQuantity ? null : 1,
|
||||
unit: displayUnit,
|
||||
qty: needsQuantity ? null : finalQty,
|
||||
unit: finalUnit,
|
||||
});
|
||||
|
||||
// Si necesita cantidad, preguntar
|
||||
@@ -836,9 +900,13 @@ async function processPendingClarification({ tenantId, text, nlu, order, pending
|
||||
|
||||
// Listo, mover al cart
|
||||
const finalOrder = moveReadyToCart(updatedOrder);
|
||||
// Formatear la cantidad según la unidad
|
||||
const qtyDisplay = displayUnit === "unit"
|
||||
? `${finalQty} ${finalQty === 1 ? 'unidad de' : 'unidades de'}`
|
||||
: `${finalQty}${displayUnit}`;
|
||||
return {
|
||||
plan: {
|
||||
reply: `Perfecto, anoto 1 ${selected.name}. ¿Algo más?`,
|
||||
reply: `Perfecto, anoto ${qtyDisplay} ${selected.name}. ¿Algo más?`,
|
||||
next_state: ConversationState.CART,
|
||||
intent: "add_to_cart",
|
||||
missing_fields: [],
|
||||
@@ -848,6 +916,69 @@ async function processPendingClarification({ tenantId, text, nlu, order, pending
|
||||
};
|
||||
}
|
||||
|
||||
// Si no hay candidatos (producto no encontrado) y el usuario da texto libre,
|
||||
// intentar re-buscar con el texto como aclaración
|
||||
if ((!pendingItem.candidates || pendingItem.candidates.length === 0) && text && text.length > 2) {
|
||||
const newQuery = text.trim();
|
||||
const searchResult = await retrieveCandidates({ tenantId, query: newQuery, limit: 20 });
|
||||
const newCandidates = searchResult?.candidates || [];
|
||||
audit.retry_search = { query: newQuery, count: newCandidates.length };
|
||||
|
||||
if (newCandidates.length > 0) {
|
||||
// Encontramos opciones con la nueva búsqueda
|
||||
const updatedPending = {
|
||||
...pendingItem,
|
||||
query: newQuery,
|
||||
candidates: newCandidates.slice(0, 20).map(c => ({
|
||||
woo_id: c.woo_product_id,
|
||||
name: c.name,
|
||||
price: c.price,
|
||||
sell_unit: c.sell_unit || null,
|
||||
display_unit: normalizeUnit(c.sell_unit) || inferDefaultUnit({ name: c.name, categories: c.categories }),
|
||||
})),
|
||||
};
|
||||
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
pending: order.pending.map(p => p.id === pendingItem.id ? updatedPending : p),
|
||||
};
|
||||
|
||||
const { question } = formatOptionsForDisplay(updatedPending);
|
||||
return {
|
||||
plan: {
|
||||
reply: question,
|
||||
next_state: ConversationState.CART,
|
||||
intent: "add_to_cart",
|
||||
missing_fields: ["product_selection"],
|
||||
order_action: "none",
|
||||
},
|
||||
decision: { actions: [], order: updatedOrder, audit },
|
||||
};
|
||||
}
|
||||
|
||||
// Sigue sin encontrar después de la aclaración -> escalar a humano
|
||||
// Primero eliminar el pending item que no se puede resolver
|
||||
const orderWithoutPending = {
|
||||
...order,
|
||||
pending: (order.pending || []).filter(p => p.id !== pendingItem.id),
|
||||
};
|
||||
|
||||
audit.escalated_to_human = true;
|
||||
audit.original_query = pendingItem.query;
|
||||
audit.retry_query = newQuery;
|
||||
|
||||
// Crear takeover con contexto de la búsqueda fallida
|
||||
return createHumanTakeoverResponse({
|
||||
pendingQuery: `${pendingItem.query} (aclaración: "${newQuery}")`,
|
||||
order: orderWithoutPending,
|
||||
context: {
|
||||
original_query: pendingItem.query,
|
||||
user_clarification: newQuery,
|
||||
search_attempts: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// No entendió, volver a preguntar
|
||||
const { question } = formatOptionsForDisplay(pendingItem);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user