carrito semi saneado

This commit is contained in:
Lucas Tettamanti
2026-01-17 15:20:32 -03:00
parent 204403560e
commit 8cc4744c49
4 changed files with 48 additions and 40 deletions

View File

@@ -276,6 +276,9 @@ class ConversationInspector extends HTMLElement {
this.rowMap.set(msgId, optItem); this.rowMap.set(msgId, optItem);
this.rowOrder.push(msgId); this.rowOrder.push(msgId);
} }
// Auto-scroll al final
list.scrollTop = list.scrollHeight;
} }
applyHeights() { applyHeights() {

View File

@@ -197,10 +197,14 @@ class RunTimeline extends HTMLElement {
addedOptimistic = true; addedOptimistic = true;
} }
// auto-scroll solo si agregamos burbujas optimistas nuevas // auto-scroll al final cuando hay mensajes nuevos
if (addedOptimistic) { // Solo si el usuario estaba cerca del final (dentro de 150px) o si había optimistas
const wasNearBottom = this._lastScrollPosition === undefined ||
(log.scrollHeight - this._lastScrollPosition - log.clientHeight) < 150;
if (addedOptimistic || wasNearBottom) {
log.scrollTop = log.scrollHeight; log.scrollTop = log.scrollHeight;
} }
this._lastScrollPosition = log.scrollTop;
requestAnimationFrame(() => this.emitLayout()); requestAnimationFrame(() => this.emitLayout());
this.bindScroll(log); this.bindScroll(log);
@@ -211,6 +215,7 @@ class RunTimeline extends HTMLElement {
if (this._scrollBound) return; if (this._scrollBound) return;
this._scrollBound = true; this._scrollBound = true;
log.addEventListener("scroll", () => { log.addEventListener("scroll", () => {
this._lastScrollPosition = log.scrollTop;
emit("ui:chatScroll", { chat_id: this.chatId, scrollTop: log.scrollTop }); emit("ui:chatScroll", { chat_id: this.chatId, scrollTop: log.scrollTop });
}); });
} }

View File

@@ -162,10 +162,7 @@ let externalCustomerId = await getExternalCustomerIdByChat({
logStage(stageDebug, "history", { has_history: Array.isArray(conversation_history), state: prev_state }); logStage(stageDebug, "history", { has_history: Array.isArray(conversation_history), state: prev_state });
let reducedContext = prev?.context && typeof prev?.context === "object" ? { ...prev.context } : {}; let reducedContext = prev?.context && typeof prev?.context === "object" ? { ...prev.context } : {};
// #region agent log let decision;
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"pipeline.js:164",message:"pipeline_loaded_context",data:{prev_state,has_prev_context:!!prev?.context,reducedContext_has_order_basket:!!reducedContext?.order_basket,reducedContext_basket_count:reducedContext?.order_basket?.items?.length||0,reducedContext_basket_labels:(reducedContext?.order_basket?.items||[]).map(i=>i.label)},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H3-H4"})}).catch(()=>{});
// #endregion
let decision;
let plan; let plan;
let llmMeta; let llmMeta;
let tools = []; let tools = [];
@@ -341,12 +338,7 @@ const runStatus = llmMeta?.error ? "warn" : "ok";
railguard: { simulated: isSimulated, source: meta?.source || null }, railguard: { simulated: isSimulated, source: meta?.source || null },
woo_customer_error: wooCustomerError, woo_customer_error: wooCustomerError,
}; };
const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state;
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"pipeline.js:345",message:"pipeline_saving_context",data:{reducedContext_basket_count:reducedContext?.order_basket?.items?.length||0,context_patch_basket_count:decision?.context_patch?.order_basket?.items?.length||0,final_context_basket_count:context?.order_basket?.items?.length||0,final_context_basket_labels:(context?.order_basket?.items||[]).map(i=>i.label),plan_intent:plan?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H2-H4"})}).catch(()=>{});
// #endregion
const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state;
plan.next_state = nextState; plan.next_state = nextState;
const stateRow = await upsertConversationState({ const stateRow = await upsertConversationState({

View File

@@ -367,12 +367,7 @@ function applyTypeSelection(item, selection, text) {
const hasExplicitUnit = item.unit != null && item.unit !== ""; const hasExplicitUnit = item.unit != null && item.unit !== "";
const quantityIsGeneric = hasQty && Number(item.quantity) <= 2 && !hasExplicitUnit; const quantityIsGeneric = hasQty && Number(item.quantity) <= 2 && !hasExplicitUnit;
const needsQuantityClarification = sellsByWeight && quantityIsGeneric; const needsQuantityClarification = sellsByWeight && quantityIsGeneric;
return {
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:applyTypeSelection",message:"type_selection_qty_check",data:{product_name:resolvedProduct?.name,display_unit:resolvedProduct?.display_unit,item_quantity:item.quantity,item_unit:item.unit,hasQty,sellsByWeight,hasExplicitUnit,quantityIsGeneric,needsQuantityClarification,final_status:needsQuantityClarification?"NEEDS_QUANTITY":"READY"},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H-qty"})}).catch(()=>{});
// #endregion
return {
...item, ...item,
candidates: [], candidates: [],
resolved: resolvedProduct, resolved: resolvedProduct,
@@ -464,12 +459,7 @@ async function initializePendingItems({
audit.catalog_init = audit.catalog_init || []; audit.catalog_init = audit.catalog_init || [];
audit.catalog_init.push({ query: mention.query, count: candidates?.length || 0 }); audit.catalog_init.push({ query: mention.query, count: candidates?.length || 0 });
const item = createPendingItem({
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:initializePendingItems",message:"creating_pending_item",data:{query:mention.query,quantity:mention.quantity,unit:mention.unit,candidates_count:candidates?.length||0},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H-init"})}).catch(()=>{});
// #endregion
const item = createPendingItem({
id: `pi_${timestamp}_${i}`, id: `pi_${timestamp}_${i}`,
query: mention.query, query: mention.query,
quantity: mention.quantity, quantity: mention.quantity,
@@ -901,12 +891,7 @@ export async function runTurnV3({
const actions = []; const actions = [];
const context_patch = {}; const context_patch = {};
const audit = {}; const audit = {};
// Observabilidad (NO se envía al LLM)
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:898",message:"runTurnV3_entry",data:{prev_state,text_preview:String(text||"").slice(0,50),has_prev_context:!!prev_context,prev_order_basket_exists:!!prev?.order_basket,prev_basket_items_count:prev?.order_basket?.items?.length||0,prev_basket_labels:(prev?.order_basket?.items||[]).map(i=>i.label)},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H3-H4"})}).catch(()=>{});
// #endregion
// Observabilidad (NO se envía al LLM)
audit.trace = { audit.trace = {
tenantId: tenantId || null, tenantId: tenantId || null,
chat_id: chat_id || null, chat_id: chat_id || null,
@@ -929,12 +914,25 @@ export async function runTurnV3({
}; };
const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluInput }); const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluInput });
audit.nlu = { raw_text, model, usage, validation, parsed: nlu }; audit.nlu = { raw_text, model, usage, validation, parsed: nlu };
// ─────────────────────────────────────────────────────────────
// PRIORIDAD: Checkout steps (payment/shipping) tienen prioridad sobre pending_items
// ─────────────────────────────────────────────────────────────
const isInCheckoutFlow = prev?.checkout_step === "payment_method" || prev?.checkout_step === "shipping_method";
const isNumericCheckoutResponse = /^\s*[12]\s*$/.test(String(text || ""));
const skipPendingItemsForCheckout = isInCheckoutFlow && isNumericCheckoutResponse;
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:checkout_priority_check",message:"checkout_flow_check",data:{checkout_step:prev?.checkout_step,isInCheckoutFlow,isNumericCheckoutResponse,skipPendingItemsForCheckout,text,nlu_intent:nlu?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H-checkout"})}).catch(()=>{});
// #endregion
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// NUEVO FLUJO: Carrito acumulativo con pending_items // NUEVO FLUJO: Carrito acumulativo con pending_items
// (Solo si NO estamos en checkout flow con respuesta numérica)
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// 0a) Si hay pending_items existentes, continuar clarificación // 0a) Si hay pending_items existentes, continuar clarificación
if (Array.isArray(prev?.pending_items) && prev.pending_items.length > 0) { if (!skipPendingItemsForCheckout && Array.isArray(prev?.pending_items) && prev.pending_items.length > 0) {
const hasPendingToProcess = prev.pending_items.some( const hasPendingToProcess = prev.pending_items.some(
i => i.status === PENDING_ITEM_STATUS.NEEDS_TYPE || i.status === PENDING_ITEM_STATUS.NEEDS_QUANTITY i => i.status === PENDING_ITEM_STATUS.NEEDS_TYPE || i.status === PENDING_ITEM_STATUS.NEEDS_QUANTITY
); );
@@ -953,13 +951,14 @@ const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluI
} }
// 0b) Si el NLU detecta productos y no hay pending_items, inicializar nuevo flujo // 0b) Si el NLU detecta productos y no hay pending_items, inicializar nuevo flujo
// (Solo si NO estamos en checkout flow con respuesta numérica)
const nluIntent = nlu?.intent || "other"; const nluIntent = nlu?.intent || "other";
const isProductIntent = ["add_to_cart", "browse", "price_query"].includes(nluIntent); const isProductIntent = ["add_to_cart", "browse", "price_query"].includes(nluIntent);
const hasProducts = hasProductMentions(nlu); const hasProducts = hasProductMentions(nlu);
const noPendingItems = !Array.isArray(prev?.pending_items) || prev.pending_items.length === 0; const noPendingItems = !Array.isArray(prev?.pending_items) || prev.pending_items.length === 0;
const noLegacyPending = !prev?.pending_clarification?.candidates?.length && !prev?.pending_item?.product_id; const noLegacyPending = !prev?.pending_clarification?.candidates?.length && !prev?.pending_item?.product_id;
if (isProductIntent && hasProducts && noPendingItems && noLegacyPending) { if (!skipPendingItemsForCheckout && isProductIntent && hasProducts && noPendingItems && noLegacyPending) {
const result = await initializePendingItems({ const result = await initializePendingItems({
tenantId, tenantId,
nlu, nlu,
@@ -977,7 +976,9 @@ const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluI
// 0) Procesar multi-items si hay varios productos en un mensaje (LEGACY) // 0) Procesar multi-items si hay varios productos en un mensaje (LEGACY)
// Solo si no hay pending_clarification ni pending_item (flujo limpio) // Solo si no hay pending_clarification ni pending_item (flujo limpio)
// Y NO estamos en checkout flow
if ( if (
!skipPendingItemsForCheckout &&
Array.isArray(nlu?.entities?.items) && Array.isArray(nlu?.entities?.items) &&
nlu.entities.items.length > 0 && nlu.entities.items.length > 0 &&
!prev?.pending_clarification?.candidates?.length && !prev?.pending_clarification?.candidates?.length &&
@@ -998,7 +999,8 @@ const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluI
} }
// 1) Resolver pending_clarification primero (LEGACY) // 1) Resolver pending_clarification primero (LEGACY)
if (prev?.pending_clarification?.candidates?.length) { // Saltar si estamos en checkout flow
if (!skipPendingItemsForCheckout && prev?.pending_clarification?.candidates?.length) {
const resolved = resolvePendingSelection({ text, nlu, pending: prev.pending_clarification }); const resolved = resolvePendingSelection({ text, nlu, pending: prev.pending_clarification });
if (resolved.kind === "more") { if (resolved.kind === "more") {
const nextPending = resolved.pending || prev.pending_clarification; const nextPending = resolved.pending || prev.pending_clarification;
@@ -1367,7 +1369,12 @@ if (qty?.quantity) {
} }
// Handler para select_payment: usuario elige método de pago // Handler para select_payment: usuario elige método de pago
if (intent === "select_payment" || (prev?.checkout_step === "payment_method" && (nlu?.entities?.payment_method || nlu?.entities?.selection))) { // Detectar número simple como selección cuando estamos en checkout_step payment_method
const isPaymentSelection = prev?.checkout_step === "payment_method" && /^\s*[12]\s*$/.test(String(text || ""));
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:select_payment_check",message:"payment_handler_entry",data:{intent,checkout_step:prev?.checkout_step,text,isPaymentSelection,has_payment_method:!!nlu?.entities?.payment_method,has_selection:!!nlu?.entities?.selection,nlu_intent:nlu?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H1-H4"})}).catch(()=>{});
// #endregion
if (intent === "select_payment" || isPaymentSelection || (prev?.checkout_step === "payment_method" && (nlu?.entities?.payment_method || nlu?.entities?.selection))) {
const basketItems = Array.isArray(prev?.order_basket?.items) ? prev.order_basket.items : []; const basketItems = Array.isArray(prev?.order_basket?.items) ? prev.order_basket.items : [];
// Determinar método de pago // Determinar método de pago
@@ -1419,7 +1426,12 @@ if (qty?.quantity) {
} }
// Handler para select_shipping: usuario elige delivery o retiro // Handler para select_shipping: usuario elige delivery o retiro
if (intent === "select_shipping" || (prev?.checkout_step === "shipping_method" && (nlu?.entities?.shipping_method || nlu?.entities?.selection))) { // Detectar número simple como selección cuando estamos en checkout_step shipping_method
const isShippingSelection = prev?.checkout_step === "shipping_method" && /^\s*[12]\s*$/.test(String(text || ""));
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:select_shipping_check",message:"shipping_handler_entry",data:{intent,checkout_step:prev?.checkout_step,text,isShippingSelection,has_shipping_method:!!nlu?.entities?.shipping_method,has_selection:!!nlu?.entities?.selection,nlu_intent:nlu?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H1-H4"})}).catch(()=>{});
// #endregion
if (intent === "select_shipping" || isShippingSelection || (prev?.checkout_step === "shipping_method" && (nlu?.entities?.shipping_method || nlu?.entities?.selection))) {
const basketItems = Array.isArray(prev?.order_basket?.items) ? prev.order_basket.items : []; const basketItems = Array.isArray(prev?.order_basket?.items) ? prev.order_basket.items : [];
// Determinar método de envío // Determinar método de envío
@@ -1661,11 +1673,7 @@ if (qty?.quantity) {
// Agregar nuevo item // Agregar nuevo item
prevItems.push(item); prevItems.push(item);
} }
context_patch.order_basket = { items: prevItems };
// #region agent log
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:1645",message:"add_to_cart_basket_state",data:{prev_basket_exists:!!prev?.order_basket,prev_basket_items_count:prev?.order_basket?.items?.length||0,prev_items_labels:prevItems.map(i=>i.label),new_item_label:item.label,new_item_qty:item.quantity,new_item_unit:item.unit,was_update:existingIdx>=0},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H1-H2"})}).catch(()=>{});
// #endregion
context_patch.order_basket = { items: prevItems };
actions.push({ type: actionType, payload: item }); actions.push({ type: actionType, payload: item });
const { next_state, validation: v } = safeNextState(prev_state, { ...prev, ...context_patch }, { pending_item_completed: true }); const { next_state, validation: v } = safeNextState(prev_state, { ...prev, ...context_patch }, { pending_item_completed: true });
const display = qty.display_unit === "kg" const display = qty.display_unit === "kg"