Spaces:
Sleeping
Sleeping
| 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 }; |