Foco: matar repetición y adaptar respuestas. Los handlers tenían ~30 strings hardcodeadas (3-7 lugares cada una). Aliases hacían substring exacto. - pg_trgm + GIN indexes en product_aliases / alias_product_mappings. Captura plurales, diminutivos, typos sin reglas. catalogRetrieval re-busca el snapshot con normalized_alias cuando el query original no rinde (vasio→vacio→Vacío). - reply_templates table + replyTemplates.js. 20 keys, 2-3 variantes c/u con DEFAULTS hardcodeados como fallback. pickVariant excluye las usadas en context.recent_replies (FIFO cap 8). Wired en idle/cart/cartHelpers/ shipping/payment/waiting. - failed_searches counter en context. count>=3 escala via humanFallback. Reset en cada add_to_cart exitoso. - storeContext.js: vars derivadas de getStoreConfig (delivery_zones, hours, zonas) listas para inyectar en templates cuando los datos se carguen. - replyRewriter.js: LLM call opcional (REPLY_REWRITER=1) que adapta el template al hilo conversacional. 1.5s timeout, fallback al template puro. Sólo activo en 8 slots semánticamente importantes. - 12 unit tests para replyTemplates (rotation, recency, FIFO, vars). 208 tests totales pasando. Plan completo: ~/.claude/plans/ok-creo-que-tiene-humming-sutton.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
4.1 KiB
JavaScript
134 lines
4.1 KiB
JavaScript
/**
|
|
* Handler para el estado WAITING_WEBHOOKS
|
|
*/
|
|
|
|
import { ConversationState } from "../fsm.js";
|
|
import { createEmptyOrder, formatCartForDisplay } from "../orderModel.js";
|
|
import { renderReply } from "../replyTemplates.js";
|
|
|
|
/**
|
|
* Detecta si el usuario pregunta por horarios/días de entrega
|
|
*/
|
|
function isDeliveryInfoQuestion(text) {
|
|
const t = String(text || "").toLowerCase();
|
|
const patterns = [
|
|
/cu[aá]ndo\s+(entrega|llega|env[ií])/i,
|
|
/qu[eé]\s+d[ií]as?\s+(entrega|env[ií]|repart)/i,
|
|
/d[ií]as?\s+de\s+(entrega|env[ií]|reparto)/i,
|
|
/horario\s+de\s+(entrega|env[ií]|reparto)/i,
|
|
/qu[eé]\s+horarios?/i,
|
|
/a\s+qu[eé]\s+hora/i,
|
|
/en\s+qu[eé]\s+horario/i,
|
|
/cuando\s+(me\s+)?lo\s+traen/i,
|
|
/d[ií]a\s+y\s+hora/i,
|
|
];
|
|
return patterns.some(p => p.test(t));
|
|
}
|
|
|
|
/**
|
|
* Detecta si el usuario pregunta por horarios de retiro
|
|
*/
|
|
function isPickupInfoQuestion(text) {
|
|
const t = String(text || "").toLowerCase();
|
|
const patterns = [
|
|
/horario.*(retir|buscar|pasar)/i,
|
|
/cu[aá]ndo.*(retir|buscar|pasar)/i,
|
|
/a\s+qu[eé]\s+hora.*(retir|buscar)/i,
|
|
/d[ií]as?.*(retir|buscar)/i,
|
|
];
|
|
return patterns.some(p => p.test(t));
|
|
}
|
|
|
|
/**
|
|
* Maneja el estado WAITING_WEBHOOKS (esperando confirmación de pago)
|
|
*/
|
|
export async function handleWaitingState({ tenantId, text, nlu, order, audit, storeConfig = {}, recentReplies }) {
|
|
const intent = nlu?.intent || "other";
|
|
const currentOrder = order || createEmptyOrder();
|
|
|
|
// view_cart
|
|
if (intent === "view_cart") {
|
|
const cartDisplay = formatCartForDisplay(currentOrder);
|
|
const status = currentOrder.is_paid ? "✓ Pagado" : "Esperando pago...";
|
|
return {
|
|
plan: {
|
|
reply: `${cartDisplay}\n\nEstado: ${status}`,
|
|
next_state: ConversationState.WAITING_WEBHOOKS,
|
|
intent: "view_cart",
|
|
missing_fields: [],
|
|
order_action: "none",
|
|
},
|
|
decision: { actions: [], order: currentOrder, audit },
|
|
};
|
|
}
|
|
|
|
// Preguntas sobre horarios/días de entrega
|
|
if (isDeliveryInfoQuestion(text)) {
|
|
// Usar deliveryHours que ya viene formateado desde getStoreConfig
|
|
// (agrupa días con mismos horarios: "Lunes a Viernes de 9:00 a 14:00, Sábado de 9:00 a 13:00")
|
|
const deliveryHours = storeConfig.deliveryHours;
|
|
|
|
let reply = "";
|
|
if (deliveryHours && deliveryHours !== "No disponible") {
|
|
reply = `Hacemos entregas: ${deliveryHours}. `;
|
|
} else if (storeConfig.deliveryEnabled === false) {
|
|
reply = "Por el momento no ofrecemos delivery. ";
|
|
} else {
|
|
reply = "Todavía no tengo configurados los horarios de entrega. ";
|
|
}
|
|
|
|
reply += "Tu pedido ya está en proceso, avisame cualquier cosa.";
|
|
|
|
return {
|
|
plan: {
|
|
reply,
|
|
next_state: ConversationState.WAITING_WEBHOOKS,
|
|
intent: "delivery_info",
|
|
missing_fields: [],
|
|
order_action: "none",
|
|
},
|
|
decision: { actions: [], order: currentOrder, audit },
|
|
};
|
|
}
|
|
|
|
// Preguntas sobre horarios de retiro
|
|
if (isPickupInfoQuestion(text)) {
|
|
const pickupHours = storeConfig.pickupHours;
|
|
|
|
let reply = "";
|
|
if (pickupHours && pickupHours !== "No disponible") {
|
|
reply = `Podés retirar: ${pickupHours}. `;
|
|
} else if (storeConfig.pickupEnabled === false) {
|
|
reply = "Por el momento no ofrecemos retiro en tienda. ";
|
|
} else {
|
|
reply = "Todavía no tengo configurados los horarios de retiro. ";
|
|
}
|
|
|
|
reply += "Tu pedido ya está en proceso, avisame cualquier cosa.";
|
|
|
|
return {
|
|
plan: {
|
|
reply,
|
|
next_state: ConversationState.WAITING_WEBHOOKS,
|
|
intent: "pickup_info",
|
|
missing_fields: [],
|
|
order_action: "none",
|
|
},
|
|
decision: { actions: [], order: currentOrder, audit },
|
|
};
|
|
}
|
|
|
|
// Default
|
|
const r = await renderReply({ tenantId, templateKey: "waiting.in_progress", recentReplies });
|
|
return {
|
|
plan: {
|
|
reply: r.reply,
|
|
next_state: ConversationState.WAITING_WEBHOOKS,
|
|
intent: "other",
|
|
missing_fields: [],
|
|
order_action: "none",
|
|
},
|
|
decision: { actions: [], order: currentOrder, audit, template_ids_used: [r.template_id] },
|
|
};
|
|
}
|