pini-print-bot / controllers /chatController.js
dotandru's picture
feat(v5.7.4): Semantic Closure Gate, Delete unused files, PDF Fixes
d897af7
Raw
History Blame Contribute Delete
13.6 kB
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 };