Spaces:
Sleeping
Sleeping
| const { classifyMessage } = require('../engine/classifier'); | |
| const { compileOrder, buildSessionStateContext, getJITKnowledge, normalizeEntity } = require('../engine/planner'); | |
| const { getSession, checkAndLockRequest, cacheCompletedRequest, releaseFailedRequest, pushToSessionCart } = require('../services/sessionManager'); | |
| const { processImageUpload } = require('../engine/imageProcessor'); | |
| const { analyzePrintReadyStatus } = require('../engine/decisionKernel'); | |
| const { calculate_custom_job } = require('../engine/calculation'); | |
| const DOMAIN_TEMPLATES = require('../db/domainTemplates.json'); | |
| async function handleChat(req, res) { | |
| const { message, userId, requestId } = req.body; | |
| const sessionID = userId || 'default_user'; | |
| const session = getSession(sessionID); | |
| if (!requestId || !/^[0-9a-fA-F-]{36}$/.test(requestId)) { | |
| return res.status(400).json({ text: "Bad Request: Invalid requestId UUID." }); | |
| } | |
| const lockResult = checkAndLockRequest(session, requestId, message); | |
| if (lockResult.status === 'PROCESSING_ERROR') return res.status(429).json({ text: "ืื ื ืืืจ ืืขืื ืืช ืืืงืฉื ืืื..." }); | |
| if (lockResult.status === 'COMPLETED') return res.json(lockResult.cachedResponse); | |
| // --- Spec v5.7.4: Hard Bypass for Locked Sessions --- | |
| if (session.state === "locked") { | |
| console.log("๐ [HARD GATE] LLM Bypass Activated."); | |
| return res.json(generate_deterministic_summary(session)); | |
| } | |
| try { | |
| // 0. The blockState UI refusal has been MOVED downstream to allow decoupled parsing! | |
| // --- Spec v5.7: Stateful Advisor Preparation --- | |
| session.statefulContext = buildSessionStateContext(session); | |
| if (session.addToHistory) session.addToHistory('user', message); | |
| // 1. Comprehension (Extractor) | |
| const extraction = await classifyMessage(message, session); | |
| console.log("๐งฉ [EXTRACTION DATA]:", JSON.stringify(extraction, null, 2)); | |
| // --- Spec v5.7.4: Semantic Gate Evaluation --- | |
| const activeProds = session.active_products || []; | |
| const allItemsPriced = activeProds.length > 0 && activeProds.every(p => p.status === 'priced' || p.status === 'confirmed'); | |
| if (allItemsPriced && extraction.confirmation_flag && extraction.closure_intent_score > 0.8) { | |
| console.log("๐ [CLOSURE GATE] Semantic conditions met. Locking session."); | |
| session.state = "locked"; | |
| const finalPayload = generate_deterministic_summary(session); | |
| cacheCompletedRequest(session, requestId, finalPayload); | |
| return res.json(finalPayload); | |
| } | |
| // --- DECOUPLING BLOCKSTATE (Spec v5.7.3) --- | |
| if (session.blockState && session.blockState.reason === "PARAMETER_INSTABILITY") { | |
| // Check if the user is answering the block or completely changing topics | |
| let isSwitchingProduct = false; | |
| if (extraction.products_detected && extraction.products_detected.length > 0) { | |
| const requestedProduct = normalizeEntity(extraction.products_detected[0].product); | |
| if (session.currentProduct !== requestedProduct) isSwitchingProduct = true; | |
| } | |
| if (isSwitchingProduct || extraction.intent === 'reset') { | |
| console.log("๐ [DECOUPLING] Detected context switch. Dropping TTL block."); | |
| session.blockState = { reason: null, ttl: 0 }; | |
| } else { | |
| const blockRefusal = `ืื ื ืขืืืื ืืืื ืฉืชืืฆืข ืกืืจ ืืืืื. ืชืืื ืืืฉืจ ืืืืช ืืืช ืืืืืงืช ืขืืืจ ืืืืฆืจ ืฉืฉืื ืืช? (ืืกืืื ืชืฉืชืืจืจ ืืขืื ${session.blockState.ttl} ืืืืขืืช)`; | |
| return res.json({ text: blockRefusal, cart: session.cart }); | |
| } | |
| } | |
| // 2. Build JIT Knowledge (Mini-RAG) - Now Intent-Aware | |
| session.jitKnowledge = getJITKnowledge(message, extraction); | |
| let finalResponse = { text: "ืฉืืืื ืคื ืืืืช.", options: [], cart: session.cart }; | |
| // --- LAYER 1: Deterministic Vision Rejection --- | |
| if (extraction.technicalMetadata) { | |
| const kernelVerdict = analyzePrintReadyStatus(extraction.technicalMetadata, session); | |
| if (kernelVerdict.status === 'REJECT_LOW_RES') { | |
| const refusal = `ืขืฆืืจ! ืืืขืจืืช ืืืืชื ืฉืืงืืืฅ ืืจืืืืืฆืื ื ืืืื ืืื ืืืืคืกื (${extraction.technicalMetadata.dpi} DPI). ื ืืจืฉ ืืคืืืช 150 DPI ืืชืืฆืื ืืืืืชืืช.\n\n[ืืขืจื ืกืื ืืืช]: ${extraction.answer_text}`; | |
| finalResponse.text = refusal; | |
| finalResponse.status = 'REJECTED'; | |
| finalResponse.debug = extraction._debug; | |
| if (session.addToHistory) session.addToHistory('assistant', refusal); | |
| cacheCompletedRequest(session, requestId, finalResponse); | |
| return res.json(finalResponse); | |
| } | |
| } | |
| // 2. Handle System Intent vs Compiler Flow | |
| // --- IMMUTABLE RESET GUARDRAIL (Spec v5.7.2) --- | |
| if (extraction.intent === 'reset') { | |
| const hasNumbers = /\d+/.test(message); | |
| const hasProductName = Object.keys(DOMAIN_TEMPLATES.products || {}).some(prod => | |
| message.includes(prod) || message.includes(DOMAIN_TEMPLATES.products[prod].label) | |
| ); | |
| const hasSpecs = ["ืืจืืื", "ืืืื ืฆืื", "ืืื", "ืื", "ืืืจืืง", "ื ืืืจ", "ืงืืคืื"].some(s => message.includes(s)); | |
| if (message.split(' ').length > 4 || hasNumbers || hasProductName || hasSpecs) { | |
| console.log(`๐ก๏ธ [GUARDRAIL] Intercepted false 'reset' intent. Defaulting to 'chat'. Input: "${message}"`); | |
| extraction.intent = 'chat'; // Override the classification | |
| } | |
| } | |
| if (extraction.intent === 'reset') { | |
| session.cart = []; | |
| session.currentProduct = null; | |
| session.active_products = []; | |
| finalResponse.text = "ืืืขืจืืช ืืืคืกื. ืื ืชืจืฆื ืืืืืื?"; | |
| } | |
| else if (extraction.intent === 'show_cart') { | |
| finalResponse.text = session.cart.length > 0 ? "ืื ืื ืฉืืฉ ืื ื ืืื ืชืืื:" : "ืืขืืื ืจืืงื."; | |
| } | |
| else { | |
| // 3. Planning (Conversational Compiler v5.7) | |
| const compilation = compileOrder(extraction, session); | |
| // --- SMART RESPONSE SELECTION (BLOCKER FIX: Priority logic) --- | |
| // RULE: LLM 'answer_text' is the primary voice. Fallback guidance only if LLM fails or is too short. | |
| let responseMsg = extraction.answer_text || ""; | |
| if (compilation.status === 'READY' || compilation.status === 'PARTIAL_READY' || (compilation.items && compilation.items.length > 0) || (compilation.deleted_items && compilation.deleted_items.length > 0)) { | |
| // A. Handle Deletions | |
| if (compilation.deleted_items && compilation.deleted_items.length > 0) { | |
| compilation.deleted_items.forEach(product => { | |
| session.cart = session.cart.filter(item => item.product !== product); | |
| // session.active_products deletion is handled by the planner | |
| }); | |
| } | |
| // B. Process READY items | |
| compilation.items.forEach(item => { | |
| session.cart = session.cart.filter(c => c.product !== item.product); | |
| const productionItem = calculate_custom_job(session.cart, { | |
| product: item.product, | |
| ...item.params | |
| }); | |
| pushToSessionCart(session, productionItem); | |
| // Spec v5.7.1: Keep the product in the session state but mark it as priced | |
| const activeProd = (session.active_products || []).find(p => p.type === item.product && p.status !== 'confirmed'); | |
| if (activeProd) activeProd.status = 'priced'; | |
| }); | |
| // C. Status Updates (Only append if LLM didn't explain the status) | |
| const draftProducts = (session.active_products || []).filter(p => p.status === 'draft').map(p => p.type); | |
| if (draftProducts.length > 0 && !responseMsg.includes("๐") && !responseMsg.includes("ืืกืจ")) { | |
| const pendingLabels = draftProducts.map(p => DOMAIN_TEMPLATES.products[p]?.label || p); | |
| const readyLabels = session.cart.map(i => i.displayName); | |
| if (readyLabels.length > 0) { | |
| responseMsg += `\n\n๐ [ืขืืืื ืกืืืืก]: ืืฉ ืืืืจ ื${readyLabels.join(' ื-')}, ืืกืจ ืืคืจื ื${pendingLabels.join(' ื-')}.`; | |
| } | |
| } | |
| } | |
| else if (compilation.status === 'CONSULTATIVE_ACTIVE' || compilation.status === 'CLARIFICATION_REQUIRED') { | |
| if (compilation.reason === 'PARAMETER_INSTABILITY') { | |
| session.blockState = { reason: "PARAMETER_INSTABILITY", ttl: 2 }; | |
| } | |
| // D. Advisor Priority Check | |
| // If LLM didn't provide a substantial response, use the static fallback guidance | |
| if (responseMsg.length < 10 && compilation.fallback_guidance && compilation.fallback_guidance.length > 0) { | |
| responseMsg = compilation.fallback_guidance.join("\n"); | |
| } | |
| } | |
| // Absolute Fallback | |
| if (!responseMsg || responseMsg.trim() === "") { | |
| responseMsg = (compilation.fallback_guidance && compilation.fallback_guidance.length > 0) | |
| ? compilation.fallback_guidance.join("\n") | |
| : "ืื ื ืืื, ืขื ืื ืชืจืฆื ืฉื ืืืจ?"; | |
| } | |
| const uiCart = [...session.cart]; | |
| (session.active_products || []).filter(p => p.status === 'draft').forEach(draftItem => { | |
| const prodKey = draftItem.type; | |
| if (!uiCart.find(c => c.product === prodKey)) { | |
| const meta = DOMAIN_TEMPLATES.products[prodKey] || {}; | |
| uiCart.push({ | |
| product: prodKey, | |
| displayName: meta.label || prodKey, | |
| isPending: true, | |
| client_price: 0, | |
| qty: draftItem.attributes?.qty || 0 | |
| }); | |
| } | |
| }); | |
| finalResponse.text = responseMsg; | |
| finalResponse.cart = uiCart; | |
| } | |
| // --- Spec v5.7: Tracking Finish --- | |
| if (session.addToHistory) session.addToHistory('assistant', finalResponse.text); | |
| session.lastBotMessage = finalResponse.text; | |
| session.lastIntent = extraction.intent; | |
| finalResponse.debug = extraction._debug; | |
| cacheCompletedRequest(session, requestId, finalResponse); | |
| res.json(finalResponse); | |
| } catch (error) { | |
| console.error("๐ฅ Controller Error:", error); | |
| releaseFailedRequest(session, requestId); | |
| if (!res.headersSent) { | |
| res.status(500).json({ text: "ืืืคืก, ื ืชืงืืชื ืืืขืื. ื ืกื ืฉืื." }); | |
| } | |
| } | |
| } | |
| async function handleImageUpload(req, res) { | |
| const { userId } = req.body; | |
| const sessionID = userId || 'default_user'; | |
| const session = getSession(sessionID); | |
| if (!req.file) return res.status(400).json({ text: "ืื ืืืขืื ืงืืืฅ." }); | |
| try { | |
| const technicalPayload = await processImageUpload(req.file.buffer); | |
| const kernelVerdict = analyzePrintReadyStatus(technicalPayload, session); | |
| if (kernelVerdict.status === 'REJECT_LOW_RES') { | |
| const refusal = `ืขืฆืืจ! ืืืขืจืืช ืืืืชื ืฉืืงืืืฅ ืืจืืืืืฆืื ื ืืืื ืืื (${technicalPayload.dpi} DPI). ื ืืจืฉ 150 DPI.`; | |
| return res.json({ text: refusal, status: kernelVerdict.status, technical: technicalPayload }); | |
| } | |
| session.lastImageMetadata = technicalPayload; | |
| return res.json({ | |
| text: "ืืงืืืฅ ื ืืืง ืืื ืืช ืื ืืฆื ืชืงืื! ืขื ืื ื ืืคืืก ืืืชื?", | |
| status: kernelVerdict.status, | |
| technical: technicalPayload, | |
| options: ['ืคืืืืจืื', 'ืคืืกืืจืื', 'ืืืืงืืช'] | |
| }); | |
| } catch (error) { | |
| console.error("๐ฅ Upload Error:", error); | |
| res.status(500).json({ text: "ืืืคืก, ื ืชืงืืชื ืืืขืื ืืขืืืื ืืชืืื ื." }); | |
| } | |
| } | |
| // --- Spec v5.7.4: Deterministic Summary Builder --- | |
| function generate_deterministic_summary(session) { | |
| let summaryText = "ืชืืื! ืืืืื ื ืฉืื ืืืื ื ืืืคืงื.\n\nืกืืืื ืืืื ื:\n"; | |
| let totalPrice = 0; | |
| const uiCart = session.cart || []; | |
| uiCart.forEach(item => { | |
| const qty = item.qty || item.quantity || 1; | |
| const price = item.pricing_snapshot?.client_price || item.client_price || 0; | |
| totalPrice += price; | |
| summaryText += `โข ${qty} ${item.displayName} - โช${price.toLocaleString()}\n`; | |
| }); | |
| summaryText += `\nืกื"ื: โช${totalPrice.toLocaleString()}\n\n`; | |
| summaryText += "ืืื ืืืื! ืืืฅ ืืขืช ืขื ืืืคืชืืจ ืืืจืืง 'ืืืจืื ื-PDF' ืื ืขื ืกืื ืืขืืื ืืื ืืงืื ืืฆืขืช ืืืืจ ืืกืืืจืช."; | |
| return { | |
| text: summaryText, | |
| cart: uiCart, | |
| locked: true | |
| }; | |
| } | |
| module.exports = { handleChat, handleImageUpload }; | |