File size: 13,629 Bytes
79c5a92
73b3a3e
1e40870
60334f4
 
bebb5ea
492565a
79c5a92
 
d14861f
79c5a92
 
 
d14861f
 
 
79c5a92
6a25ae8
d14861f
 
6a25ae8
d897af7
 
 
 
 
 
79c5a92
d6e7f9e
 
1e40870
73b3a3e
 
 
 
 
d14861f
 
3ee272e
79c5a92
d897af7
 
 
 
 
 
 
 
 
 
 
 
d6e7f9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73b3a3e
 
 
d14861f
6a25ae8
0ec8c7c
 
 
 
 
 
 
 
73b3a3e
 
0ec8c7c
 
 
 
 
d14861f
35d8bd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d14861f
 
 
35d8bd5
d14861f
 
 
 
 
 
73b3a3e
522ab0a
d14861f
73b3a3e
 
 
 
522ab0a
 
73b3a3e
522ab0a
 
 
1c434d9
522ab0a
 
 
73b3a3e
1e40870
522ab0a
1e40870
bebb5ea
1e40870
bebb5ea
73b3a3e
1c434d9
 
 
 
bebb5ea
 
73b3a3e
1c434d9
73b3a3e
492565a
 
 
73b3a3e
492565a
 
73b3a3e
 
 
 
 
492565a
73b3a3e
 
 
 
1e40870
73b3a3e
 
 
 
 
 
 
 
1e40870
73b3a3e
1c434d9
 
73b3a3e
492565a
 
 
 
 
 
1c434d9
492565a
1e40870
73b3a3e
492565a
73b3a3e
 
79c5a92
 
73b3a3e
 
 
 
 
d14861f
6a25ae8
 
79c5a92
 
d14861f
6a25ae8
1e40870
 
 
79c5a92
 
 
60334f4
 
 
 
 
73b3a3e
60334f4
 
 
 
 
 
73b3a3e
 
60334f4
 
 
 
 
 
 
 
 
 
73b3a3e
 
60334f4
 
 
d897af7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73b3a3e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
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 };