const fs = require('fs'); const path = require('path'); // Load Domain Templates (Spec v5) const templatesPath = path.join(__dirname, '../db/domainTemplates.json'); let DOMAIN_TEMPLATES = { products: {} }; try { DOMAIN_TEMPLATES = JSON.parse(fs.readFileSync(templatesPath, 'utf8')); } catch (e) { console.error("⚠️ [COMPILER] Failed to load domainTemplates.json:", e.message); } // Load Domain Knowledge (Spec v5.7) let DOMAIN_KNOWLEDGE = {}; try { DOMAIN_KNOWLEDGE = JSON.parse(fs.readFileSync(path.join(__dirname, '../db/domainKnowledge.json'), 'utf8')); } catch (e) { console.warn("⚠️ [COMPILER] No domainKnowledge.json found."); } const PRODUCT_WHITELIST = { flyer: ["qty", "paper_type", "size", "sides", "lamination", "finishing"], booklet: ["qty", "pages", "cover", "binding", "size", "paper_type", "finishings", "book_type"], sticker: ["qty", "shape", "material", "cut", "size", "lamination"], poster: ["qty", "size", "paper_type", "lamination"], bc: ["qty", "paper_type", "corners", "lamination", "finishing", "team_size", "instances"], invitation: ["qty", "size", "paper_type", "finishing", "extras"], rollup: ["qty", "size", "material"] }; const CANONICAL_PRODUCTS = { "bc": ["business card", "business cards", "cards", "visiting card", "כרטיס ביקור", "כרטיסי ביקור", "place_card", "office"], "flyer": ["flyer", "flyers", "pamphlet", "פלייר"], "rollup": ["rollup", "rollups", "roll up", "רולאפ", "roll_stickers"], "poster": ["poster", "posters", "פוסטר"], "booklet": ["book", "books", "ספר", "ספרים", "חוברת", "חוברות"] }; function normalizeEntity(rawName) { if (!rawName) return rawName; const cleanName = rawName.toLowerCase().trim().replace(/_/g, " "); for (const [canonical, synonyms] of Object.entries(CANONICAL_PRODUCTS)) { if (synonyms.some(s => s.toLowerCase() === cleanName) || canonical === cleanName) { return canonical; } } return cleanName; } const PARAM_MAPPING = { "quantity": "qty", "amount": "qty", "paper_weight": "paper_type", "paper": "paper_type", "material_type": "material", "dimensions": "size", "multiplier": "team_size", "people": "team_size" }; /** * Deterministic Sanitizer (Spec v5.6.1) */ function applyDeterministicSanitizer(extractedData, session) { const HEBREW_BINDING_MAP = { "דבק חם": "perfect_binding", "כריכה בחום": "perfect_binding", "מילוי בצבע": "perfect_binding", "כריכה רכה": "perfect_binding", "כריכה קשה": "hard_cover", "סיכות": "saddle_stitch", "כריכת סיכות": "saddle_stitch", "ספירלה": "spiral", "סליל": "spiral" }; extractedData.parameters_detected.forEach(param => { const normContext = normalizeEntity(param.context); // Spec v5.7.3 - Hebrew Mapping Stress if (param.key === 'binding' || param.key === 'book_type' || param.key === 'finishing' || param.key === 'finishings') { for (const [hebTerm, engValue] of Object.entries(HEBREW_BINDING_MAP)) { if (String(param.value).includes(hebTerm)) { param.value = engValue; param.key = 'binding'; break; } } } if (param.key === 'qty' && normContext === 'booklet') { const val = String(param.value); const pageRegex = new RegExp(`${val}\\s*(עמודים|דפים|דף|עמוד|pages|page)`, 'i'); const reversePageRegex = new RegExp(`(עמודים|דפים|דף|עמוד|pages|page)\\s*(של|של-)?\\s*${val}`, 'i'); if (pageRegex.test(extractedData.raw_text) || reversePageRegex.test(extractedData.raw_text)) { console.log(`🛡️ [SANITIZER] Caught semantic error: '${val}' is PAGES, not QTY. Correcting...`); param.key = 'pages'; } } }); } /** * Spec v5.7: Structured Session State Builder */ function buildSessionStateContext(session) { const product = session.currentProduct || "unknown"; const activeProduct = (session.active_products || []).find(p => p.type === product && p.status !== 'confirmed') || { attributes: {} }; let missing = []; const template = DOMAIN_TEMPLATES.products[product]; if (template && template.mandatory) { missing = template.mandatory.filter(p => !activeProduct.attributes[p]); } return { current_intent: session.lastIntent || "unknown", active_product: product, extracted_slots: activeProduct.attributes, missing_mandatory_slots: missing, conversation_phase: missing.length > 0 ? "slot_filling" : (product !== "unknown" ? "ready_for_quote" : "discovery"), last_bot_question: session.lastBotMessage || "None" }; } /** * Spec v5.7: JIT Knowledge Lookup (Mini-RAG) * Enhanced to support intent-aware proactive injection. */ function getJITKnowledge(text, extraction = null) { let snippetsSet = new Set(); const cleanText = text.toLowerCase(); // 1. Keyword-based matching const isComparisonQuery = ["הבדל", "הבדלים", "שוני", "מה עדיף", "איזה עדיף", "מה ההבדל"].some(k => cleanText.includes(k)); for (const category in DOMAIN_KNOWLEDGE) { for (const itemKey in DOMAIN_KNOWLEDGE[category]) { const item = DOMAIN_KNOWLEDGE[category][itemKey]; if (item.keywords && item.keywords.some(k => cleanText.includes(k.toLowerCase()))) { snippetsSet.add(`${itemKey.toUpperCase()}: ${item.description}`); // If comparison, pull siblings in the same category if (isComparisonQuery) { for (const siblingKey in DOMAIN_KNOWLEDGE[category]) { const sibling = DOMAIN_KNOWLEDGE[category][siblingKey]; snippetsSet.add(`${siblingKey.toUpperCase()}: ${sibling.description}`); } } } } } // 2. Intent-driven proactive injection (CTO Directive) if (extraction) { const products = (extraction.products_detected || []).map(p => normalizeEntity(p.product)); const isAdviceRequest = ["איך", "כיצד", "המלצה", "הצעה", "מה כדאי", "מה מומלץ", "איך אפשר"].some(k => cleanText.includes(k)); // Helper to inject entire category const injectCategory = (cat) => { if (DOMAIN_KNOWLEDGE[cat]) { for (const key in DOMAIN_KNOWLEDGE[cat]) { snippetsSet.add(`${key.toUpperCase()}: ${DOMAIN_KNOWLEDGE[cat][key].description}`); } } }; // Targeting logic if (products.includes('bc')) { injectCategory('paper_types'); injectCategory('laminations'); } if (products.includes('booklet')) { injectCategory('paper_types'); injectCategory('bindings'); } if (isComparisonQuery || extraction.intent === 'consult' || isAdviceRequest) { injectCategory('finishes'); } } return Array.from(snippetsSet).slice(0, 8).join("\n"); } /** * Compiles LLM extracted entities into a normalized order state. */ function compileOrder(extractedData, session) { try { console.log("🧩 [COMPILER] Processing turn (v5.7.1 - Unified Product State)..."); if (!session.active_products) session.active_products = []; applyDeterministicSanitizer(extractedData, session); let buckets = {}; let fallbackGuidance = []; // Update currentProduct from extraction if found if (extractedData.products_detected.length > 0) { const firstProduct = normalizeEntity(extractedData.products_detected[0].product); if (DOMAIN_TEMPLATES.products[firstProduct]) { session.currentProduct = firstProduct; } } const deletionSignals = ["מחק", "בטל", "תוריד", "delete", "remove", "cancel"]; const isDeleteIntent = extractedData.intent === 'cancel' || deletionSignals.some(s => extractedData.raw_text.includes(s)); let deletedItems = []; extractedData.products_detected.forEach(item => { const normProduct = normalizeEntity(item.product); if (!DOMAIN_TEMPLATES.products[normProduct]) return; let activeItem = session.active_products.find(p => p.type === normProduct && p.status !== 'confirmed'); if (!activeItem) { activeItem = { type: normProduct, status: 'draft', attributes: {}, history: {} }; session.active_products.push(activeItem); } if (item.confidence >= 0.6 && activeItem.status === 'draft') activeItem.status = 'extracted'; if (isDeleteIntent && (extractedData.raw_text.includes(normProduct) || extractedData.raw_text.includes(DOMAIN_TEMPLATES.products[normProduct]?.label))) { session.active_products = session.active_products.filter(p => p !== activeItem); if (session.cart) session.cart = session.cart.filter(c => c.product !== normProduct); deletedItems.push(normProduct); } }); extractedData.parameters_detected.forEach(param => { if (PARAM_MAPPING[param.key]) param.key = PARAM_MAPPING[param.key]; const contextKey = normalizeEntity(param.context); if (contextKey === 'global') return; if (!DOMAIN_TEMPLATES.products[contextKey]) return; let activeItem = session.active_products.find(p => p.type === contextKey && p.status !== 'confirmed'); if (!activeItem) { activeItem = { type: contextKey, status: 'draft', attributes: {}, history: {} }; session.active_products.push(activeItem); } const allowedParams = PRODUCT_WHITELIST[contextKey] || []; if (allowedParams.includes(param.key)) { if (!activeItem.history[param.key]) activeItem.history[param.key] = []; activeItem.history[param.key].push(param.value); activeItem.attributes[param.key] = param.value; session.lastSpecChangeTime = Date.now(); // Spec v5.7.3/v5.7.4: If the item was already priced and we just mutated it, // downgrade it to 'extracted' so the compiler reprices it and updates the UI. if (activeItem.status === 'priced') { activeItem.status = 'extracted'; } } }); // Spec v5.7.3 - Auto-Commit Live Fix (Turn-based instability check) // We track the parameters from THIS TURN ONLY to see if the user gave conflicting inputs right now let turnHistory = {}; extractedData.parameters_detected.forEach(param => { const contextKey = normalizeEntity(param.context); if (contextKey === 'global') return; if (!turnHistory[contextKey]) turnHistory[contextKey] = {}; if (!turnHistory[contextKey][param.key]) turnHistory[contextKey][param.key] = []; turnHistory[contextKey][param.key].push(param.value); }); for (const activeItem of session.active_products) { if (activeItem.status === 'confirmed') continue; let itemUnstable = false; // Only check instability based on WHAT WAS SAID IN THIS ROUND if (turnHistory[activeItem.type]) { for (const [key, historyArray] of Object.entries(turnHistory[activeItem.type])) { const uniqueValues = new Set(historyArray.map(v => String(v))); if (uniqueValues.size > 1) itemUnstable = true; } } let finalParams = { ...activeItem.attributes }; buckets[activeItem.type] = { status: activeItem.status === 'extracted' ? "READY_FOR_INTEGRITY" : "PENDING_CONFIRMATION", params: finalParams, unstable: itemUnstable }; } for (const [product, data] of Object.entries(buckets)) { const template = DOMAIN_TEMPLATES.products[product]; if (template && template.mandatory) { const missing = template.mandatory.filter(p => !data.params[p]); if (missing.length > 0) { data.status = "NEEDS_SPECIFICATION"; // Change: This is now explicitly 'fallback' if (!fallbackGuidance.includes(template.guidance)) fallbackGuidance.push(template.guidance); } } } let validatedItems = []; let specificInstability = null; for (const [product, data] of Object.entries(buckets)) { if (data.unstable) { specificInstability = product; continue; } if (data.status === "READY_FOR_INTEGRITY") validatedItems.push({ product, params: data.params }); } return { status: fallbackGuidance.length > 0 ? "CONSULTATIVE_ACTIVE" : "READY", items: validatedItems, deleted_items: deletedItems, fallback_guidance: fallbackGuidance, reason: specificInstability ? "PARAMETER_INSTABILITY" : null }; } catch (error) { console.error("❌ [COMPILER] Error:", error); return { status: "ERROR", message: error.message }; } } module.exports = { compileOrder, buildSessionStateContext, getJITKnowledge, normalizeEntity };