Tier 1: chat quality — fuzzy aliases, reply templates, dedup, rewriter
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>
This commit is contained in:
104
src/modules/3-turn-engine/storeContext.js
Normal file
104
src/modules/3-turn-engine/storeContext.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Store Context - Helpers para inyectar info de la tienda en respuestas.
|
||||
*
|
||||
* Estos helpers leen del storeConfig ya cargado por getStoreConfig (en
|
||||
* pipeline / turnEngine) y producen variables consumibles por reply templates
|
||||
* (renderReply via {{var}}) y por el reply rewriter (como hint contextual).
|
||||
*
|
||||
* Filosofía: cuando los datos no están cargados, las vars resuelven a strings
|
||||
* vacíos / hints neutros. Los templates están diseñados para tolerar ausencia.
|
||||
*/
|
||||
|
||||
const DAY_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
||||
const DAY_KEYS_LONG = ["lunes", "martes", "miercoles", "jueves", "viernes", "sabado", "domingo"];
|
||||
|
||||
/**
|
||||
* Devuelve la clave de día (mon..sun) para hoy.
|
||||
*/
|
||||
function todayKey() {
|
||||
// Date.getDay(): 0 = domingo, 1 = lunes
|
||||
const d = new Date().getDay();
|
||||
// mapear a mon..sun
|
||||
return DAY_KEYS[(d + 6) % 7];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae el horario (string) de un day-key del schedule jsonb.
|
||||
* Formato esperado: { mon: { start: '09:00', end: '14:00', enabled: true }, ... }
|
||||
* Tolera variantes (lunes, monday, etc.) y formatos planos.
|
||||
*/
|
||||
function pickDaySlot(scheduleObj, dayIdx) {
|
||||
if (!scheduleObj || typeof scheduleObj !== "object") return null;
|
||||
const keys = [DAY_KEYS[dayIdx], DAY_KEYS_LONG[dayIdx]];
|
||||
for (const k of keys) {
|
||||
if (scheduleObj[k]) return scheduleObj[k];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatDaySlot(slot) {
|
||||
if (!slot) return null;
|
||||
if (slot.enabled === false) return null;
|
||||
const start = (slot.start || "").slice(0, 5);
|
||||
const end = (slot.end || "").slice(0, 5);
|
||||
if (!start || !end) return null;
|
||||
return `${start} a ${end}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumen de zonas de delivery: lista de barrios o "Consultar zonas".
|
||||
*/
|
||||
function summarizeDeliveryZones(deliveryZones) {
|
||||
if (!deliveryZones || typeof deliveryZones !== "object") return "";
|
||||
const names = [];
|
||||
// Soporta varios formatos:
|
||||
// 1) { caba: { barrios: ["Palermo", "Belgrano"] } }
|
||||
// 2) { zones: [{ name }] }
|
||||
// 3) { palermo: true, belgrano: true } (flat)
|
||||
if (Array.isArray(deliveryZones.zones)) {
|
||||
for (const z of deliveryZones.zones) if (z?.name) names.push(z.name);
|
||||
} else if (deliveryZones.caba?.barrios) {
|
||||
if (Array.isArray(deliveryZones.caba.barrios)) names.push(...deliveryZones.caba.barrios);
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(deliveryZones)) {
|
||||
if (v === true) names.push(k);
|
||||
else if (v?.name) names.push(v.name);
|
||||
}
|
||||
}
|
||||
if (!names.length) return "";
|
||||
if (names.length <= 5) return names.join(", ");
|
||||
return `${names.slice(0, 5).join(", ")} y otros`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye variables de contexto de tienda para usar en reply templates.
|
||||
* Cuando los datos no están, las vars vienen vacías — los templates las
|
||||
* absorben sin romper.
|
||||
*
|
||||
* @param {Object} storeConfig - resultado de getStoreConfig({ tenantId })
|
||||
* @returns {Object} vars para applyVariables / renderReply
|
||||
*/
|
||||
export function buildStoreContextVars(storeConfig = {}) {
|
||||
const dayIdx = (new Date().getDay() + 6) % 7; // 0=lunes
|
||||
const sched = storeConfig.schedule || {};
|
||||
|
||||
const deliverySlot = pickDaySlot(sched.delivery, dayIdx);
|
||||
const pickupSlot = pickDaySlot(sched.pickup, dayIdx);
|
||||
|
||||
const storeHoursToday = formatDaySlot(deliverySlot) || formatDaySlot(pickupSlot) || "";
|
||||
const deliveryAvailableNow = deliverySlot ? "sí" : (storeConfig.deliveryEnabled === false ? "no" : "");
|
||||
const deliveryZonesSummary = summarizeDeliveryZones(storeConfig.delivery_zones);
|
||||
|
||||
return {
|
||||
store_name: storeConfig.name || "",
|
||||
bot_name: storeConfig.botName || "",
|
||||
store_address: storeConfig.address || "",
|
||||
store_phone: storeConfig.phone || "",
|
||||
store_hours: storeConfig.hours || "",
|
||||
store_hours_today: storeHoursToday,
|
||||
delivery_hours: storeConfig.deliveryHours || "",
|
||||
pickup_hours: storeConfig.pickupHours || "",
|
||||
delivery_available_now: deliveryAvailableNow,
|
||||
delivery_zones_summary: deliveryZonesSummary,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user