Spaces:
Sleeping
Sleeping
V5.7.1: Unified Product State Authority - Fixed post-pricing amnesia & enabled multi-product support
Browse files- controllers/chatController.js +9 -5
- db/usage.json +2 -2
- engine/planner.js +30 -20
- sanity_results.json +94 -0
- services/sessionManager.js +4 -4
- tests/test_sanity.js +8 -8
- tests/test_v5_7_state.js +75 -0
controllers/chatController.js
CHANGED
|
@@ -80,7 +80,7 @@ async function handleChat(req, res) {
|
|
| 80 |
if (compilation.deleted_items && compilation.deleted_items.length > 0) {
|
| 81 |
compilation.deleted_items.forEach(product => {
|
| 82 |
session.cart = session.cart.filter(item => item.product !== product);
|
| 83 |
-
|
| 84 |
});
|
| 85 |
}
|
| 86 |
|
|
@@ -92,11 +92,14 @@ async function handleChat(req, res) {
|
|
| 92 |
...item.params
|
| 93 |
});
|
| 94 |
pushToSessionCart(session, productionItem);
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
| 96 |
});
|
| 97 |
|
| 98 |
// C. Status Updates (Only append if LLM didn't explain the status)
|
| 99 |
-
const draftProducts =
|
| 100 |
if (draftProducts.length > 0 && !responseMsg.includes("๐") && !responseMsg.includes("ืืกืจ")) {
|
| 101 |
const pendingLabels = draftProducts.map(p => DOMAIN_TEMPLATES.products[p]?.label || p);
|
| 102 |
const readyLabels = session.cart.map(i => i.displayName);
|
|
@@ -125,7 +128,8 @@ async function handleChat(req, res) {
|
|
| 125 |
}
|
| 126 |
|
| 127 |
const uiCart = [...session.cart];
|
| 128 |
-
|
|
|
|
| 129 |
if (!uiCart.find(c => c.product === prodKey)) {
|
| 130 |
const meta = DOMAIN_TEMPLATES.products[prodKey] || {};
|
| 131 |
uiCart.push({
|
|
@@ -133,7 +137,7 @@ async function handleChat(req, res) {
|
|
| 133 |
displayName: meta.label || prodKey,
|
| 134 |
isPending: true,
|
| 135 |
client_price: 0,
|
| 136 |
-
qty:
|
| 137 |
});
|
| 138 |
}
|
| 139 |
});
|
|
|
|
| 80 |
if (compilation.deleted_items && compilation.deleted_items.length > 0) {
|
| 81 |
compilation.deleted_items.forEach(product => {
|
| 82 |
session.cart = session.cart.filter(item => item.product !== product);
|
| 83 |
+
// session.active_products deletion is handled by the planner
|
| 84 |
});
|
| 85 |
}
|
| 86 |
|
|
|
|
| 92 |
...item.params
|
| 93 |
});
|
| 94 |
pushToSessionCart(session, productionItem);
|
| 95 |
+
|
| 96 |
+
// Spec v5.7.1: Keep the product in the session state but mark it as priced
|
| 97 |
+
const activeProd = (session.active_products || []).find(p => p.type === item.product && p.status !== 'confirmed');
|
| 98 |
+
if (activeProd) activeProd.status = 'priced';
|
| 99 |
});
|
| 100 |
|
| 101 |
// C. Status Updates (Only append if LLM didn't explain the status)
|
| 102 |
+
const draftProducts = (session.active_products || []).filter(p => p.status === 'draft').map(p => p.type);
|
| 103 |
if (draftProducts.length > 0 && !responseMsg.includes("๐") && !responseMsg.includes("ืืกืจ")) {
|
| 104 |
const pendingLabels = draftProducts.map(p => DOMAIN_TEMPLATES.products[p]?.label || p);
|
| 105 |
const readyLabels = session.cart.map(i => i.displayName);
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
const uiCart = [...session.cart];
|
| 131 |
+
(session.active_products || []).filter(p => p.status === 'draft').forEach(draftItem => {
|
| 132 |
+
const prodKey = draftItem.type;
|
| 133 |
if (!uiCart.find(c => c.product === prodKey)) {
|
| 134 |
const meta = DOMAIN_TEMPLATES.products[prodKey] || {};
|
| 135 |
uiCart.push({
|
|
|
|
| 137 |
displayName: meta.label || prodKey,
|
| 138 |
isPending: true,
|
| 139 |
client_price: 0,
|
| 140 |
+
qty: draftItem.attributes?.qty || 0
|
| 141 |
});
|
| 142 |
}
|
| 143 |
});
|
db/usage.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
{
|
| 2 |
-
"dailyCost": 0.
|
| 3 |
-
"lastReset": "2026-02-
|
| 4 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"dailyCost": 0.0012441000000000002,
|
| 3 |
+
"lastReset": "2026-02-27"
|
| 4 |
}
|
engine/planner.js
CHANGED
|
@@ -81,16 +81,16 @@ function applyDeterministicSanitizer(extractedData, session) {
|
|
| 81 |
*/
|
| 82 |
function buildSessionStateContext(session) {
|
| 83 |
const product = session.currentProduct || "unknown";
|
| 84 |
-
const
|
| 85 |
let missing = [];
|
| 86 |
const template = DOMAIN_TEMPLATES.products[product];
|
| 87 |
if (template && template.mandatory) {
|
| 88 |
-
missing = template.mandatory.filter(p => !
|
| 89 |
}
|
| 90 |
return {
|
| 91 |
current_intent: session.lastIntent || "unknown",
|
| 92 |
active_product: product,
|
| 93 |
-
extracted_slots:
|
| 94 |
missing_mandatory_slots: missing,
|
| 95 |
conversation_phase: missing.length > 0 ? "slot_filling" : (product !== "unknown" ? "ready_for_quote" : "discovery"),
|
| 96 |
last_bot_question: session.lastBotMessage || "None"
|
|
@@ -161,8 +161,8 @@ function getJITKnowledge(text, extraction = null) {
|
|
| 161 |
*/
|
| 162 |
function compileOrder(extractedData, session) {
|
| 163 |
try {
|
| 164 |
-
console.log("๐งฉ [COMPILER] Processing turn (v5.7 -
|
| 165 |
-
if (!session.
|
| 166 |
applyDeterministicSanitizer(extractedData, session);
|
| 167 |
|
| 168 |
let buckets = {};
|
|
@@ -183,12 +183,17 @@ function compileOrder(extractedData, session) {
|
|
| 183 |
extractedData.products_detected.forEach(item => {
|
| 184 |
const normProduct = normalizeEntity(item.product);
|
| 185 |
if (!DOMAIN_TEMPLATES.products[normProduct]) return;
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
-
|
|
|
|
|
|
|
| 190 |
if (isDeleteIntent && (extractedData.raw_text.includes(normProduct) || extractedData.raw_text.includes(DOMAIN_TEMPLATES.products[normProduct]?.label))) {
|
| 191 |
-
|
| 192 |
if (session.cart) session.cart = session.cart.filter(c => c.product !== normProduct);
|
| 193 |
deletedItems.push(normProduct);
|
| 194 |
}
|
|
@@ -199,28 +204,33 @@ function compileOrder(extractedData, session) {
|
|
| 199 |
const contextKey = normalizeEntity(param.context);
|
| 200 |
if (contextKey === 'global') return;
|
| 201 |
if (!DOMAIN_TEMPLATES.products[contextKey]) return;
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
| 204 |
}
|
| 205 |
-
|
| 206 |
const allowedParams = PRODUCT_WHITELIST[contextKey] || [];
|
| 207 |
if (allowedParams.includes(param.key)) {
|
| 208 |
-
if (!
|
| 209 |
-
|
| 210 |
-
|
| 211 |
session.lastSpecChangeTime = Date.now();
|
| 212 |
}
|
| 213 |
});
|
| 214 |
|
| 215 |
-
for (const
|
|
|
|
|
|
|
| 216 |
let itemUnstable = false;
|
| 217 |
-
for (const [key, history] of Object.entries(
|
| 218 |
const uniqueValues = new Set(history.map(v => String(v)));
|
| 219 |
if (uniqueValues.size > 1) itemUnstable = true;
|
| 220 |
}
|
| 221 |
-
let finalParams = { ...
|
| 222 |
-
buckets[
|
| 223 |
-
status:
|
| 224 |
params: finalParams,
|
| 225 |
unstable: itemUnstable
|
| 226 |
};
|
|
|
|
| 81 |
*/
|
| 82 |
function buildSessionStateContext(session) {
|
| 83 |
const product = session.currentProduct || "unknown";
|
| 84 |
+
const activeProduct = (session.active_products || []).find(p => p.type === product && p.status !== 'confirmed') || { attributes: {} };
|
| 85 |
let missing = [];
|
| 86 |
const template = DOMAIN_TEMPLATES.products[product];
|
| 87 |
if (template && template.mandatory) {
|
| 88 |
+
missing = template.mandatory.filter(p => !activeProduct.attributes[p]);
|
| 89 |
}
|
| 90 |
return {
|
| 91 |
current_intent: session.lastIntent || "unknown",
|
| 92 |
active_product: product,
|
| 93 |
+
extracted_slots: activeProduct.attributes,
|
| 94 |
missing_mandatory_slots: missing,
|
| 95 |
conversation_phase: missing.length > 0 ? "slot_filling" : (product !== "unknown" ? "ready_for_quote" : "discovery"),
|
| 96 |
last_bot_question: session.lastBotMessage || "None"
|
|
|
|
| 161 |
*/
|
| 162 |
function compileOrder(extractedData, session) {
|
| 163 |
try {
|
| 164 |
+
console.log("๐งฉ [COMPILER] Processing turn (v5.7.1 - Unified Product State)...");
|
| 165 |
+
if (!session.active_products) session.active_products = [];
|
| 166 |
applyDeterministicSanitizer(extractedData, session);
|
| 167 |
|
| 168 |
let buckets = {};
|
|
|
|
| 183 |
extractedData.products_detected.forEach(item => {
|
| 184 |
const normProduct = normalizeEntity(item.product);
|
| 185 |
if (!DOMAIN_TEMPLATES.products[normProduct]) return;
|
| 186 |
+
|
| 187 |
+
let activeItem = session.active_products.find(p => p.type === normProduct && p.status !== 'confirmed');
|
| 188 |
+
if (!activeItem) {
|
| 189 |
+
activeItem = { type: normProduct, status: 'draft', attributes: {}, history: {} };
|
| 190 |
+
session.active_products.push(activeItem);
|
| 191 |
}
|
| 192 |
+
|
| 193 |
+
if (item.confidence >= 0.6 && activeItem.status === 'draft') activeItem.status = 'extracted';
|
| 194 |
+
|
| 195 |
if (isDeleteIntent && (extractedData.raw_text.includes(normProduct) || extractedData.raw_text.includes(DOMAIN_TEMPLATES.products[normProduct]?.label))) {
|
| 196 |
+
session.active_products = session.active_products.filter(p => p !== activeItem);
|
| 197 |
if (session.cart) session.cart = session.cart.filter(c => c.product !== normProduct);
|
| 198 |
deletedItems.push(normProduct);
|
| 199 |
}
|
|
|
|
| 204 |
const contextKey = normalizeEntity(param.context);
|
| 205 |
if (contextKey === 'global') return;
|
| 206 |
if (!DOMAIN_TEMPLATES.products[contextKey]) return;
|
| 207 |
+
|
| 208 |
+
let activeItem = session.active_products.find(p => p.type === contextKey && p.status !== 'confirmed');
|
| 209 |
+
if (!activeItem) {
|
| 210 |
+
activeItem = { type: contextKey, status: 'draft', attributes: {}, history: {} };
|
| 211 |
+
session.active_products.push(activeItem);
|
| 212 |
}
|
| 213 |
+
|
| 214 |
const allowedParams = PRODUCT_WHITELIST[contextKey] || [];
|
| 215 |
if (allowedParams.includes(param.key)) {
|
| 216 |
+
if (!activeItem.history[param.key]) activeItem.history[param.key] = [];
|
| 217 |
+
activeItem.history[param.key].push(param.value);
|
| 218 |
+
activeItem.attributes[param.key] = param.value;
|
| 219 |
session.lastSpecChangeTime = Date.now();
|
| 220 |
}
|
| 221 |
});
|
| 222 |
|
| 223 |
+
for (const activeItem of session.active_products) {
|
| 224 |
+
if (activeItem.status === 'confirmed') continue;
|
| 225 |
+
|
| 226 |
let itemUnstable = false;
|
| 227 |
+
for (const [key, history] of Object.entries(activeItem.history || {})) {
|
| 228 |
const uniqueValues = new Set(history.map(v => String(v)));
|
| 229 |
if (uniqueValues.size > 1) itemUnstable = true;
|
| 230 |
}
|
| 231 |
+
let finalParams = { ...activeItem.attributes };
|
| 232 |
+
buckets[activeItem.type] = {
|
| 233 |
+
status: activeItem.status === 'extracted' ? "READY_FOR_INTEGRITY" : "PENDING_CONFIRMATION",
|
| 234 |
params: finalParams,
|
| 235 |
unstable: itemUnstable
|
| 236 |
};
|
sanity_results.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"test1": {
|
| 3 |
+
"input": "ืฆืจืื 1000 ืืจืืืกื ืืืงืืจ",
|
| 4 |
+
"classification": {
|
| 5 |
+
"intent": "quote",
|
| 6 |
+
"answer_text": "ืืืงืื, 1000 ืืจืืืกื ืืืงืืจ. ืืืื ืกืื ื ืืืจ ืชืจืฆื?",
|
| 7 |
+
"products_detected": [
|
| 8 |
+
{
|
| 9 |
+
"product": "bc",
|
| 10 |
+
"confidence": 0.95
|
| 11 |
+
}
|
| 12 |
+
],
|
| 13 |
+
"parameters_detected": [
|
| 14 |
+
{
|
| 15 |
+
"key": "qty",
|
| 16 |
+
"value": "1000",
|
| 17 |
+
"context": "bc",
|
| 18 |
+
"confidence": 1
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"_debug": {
|
| 22 |
+
"tokens": {
|
| 23 |
+
"in": 857,
|
| 24 |
+
"out": 133,
|
| 25 |
+
"total": 990
|
| 26 |
+
},
|
| 27 |
+
"governance": "v5.7-StatefulAdvisor"
|
| 28 |
+
},
|
| 29 |
+
"raw_text": "ืฆืจืื 1000 ืืจืืืกื ืืืงืืจ",
|
| 30 |
+
"technicalMetadata": null
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"test2": {
|
| 34 |
+
"input": "ืืืื ืคืื ื, ืื ืืขื ืืื ืื? ืื ื ืจืืฆื ืืืืคืืก ืืืืจืช ืฉืืจืื ืืกืืชื ืฉืื.",
|
| 35 |
+
"classification": {
|
| 36 |
+
"intent": "quote",
|
| 37 |
+
"answer_text": "ืฉืืื! ืื ื ืฉืื ืืขืืืจ ืื ืขื ืืืืจืช ืืฉืืจืื ืืกืืชื. ืืื ืฉืืืื ืืชืช ืื ืืฆืขืช ืืืืจ ืืืืืงืช, ืื ื ืฆืจืื ืืืขืช ืืื ืขืืืืื ืืืื ืืืืืจืช ืืืื ืขืืชืงืื ืชืจืฆื ืืืืคืืก?",
|
| 38 |
+
"products_detected": [
|
| 39 |
+
{
|
| 40 |
+
"product": "booklet",
|
| 41 |
+
"confidence": 0.95
|
| 42 |
+
}
|
| 43 |
+
],
|
| 44 |
+
"parameters_detected": [],
|
| 45 |
+
"_debug": {
|
| 46 |
+
"tokens": {
|
| 47 |
+
"in": 871,
|
| 48 |
+
"out": 124,
|
| 49 |
+
"total": 995
|
| 50 |
+
},
|
| 51 |
+
"governance": "v5.7-StatefulAdvisor"
|
| 52 |
+
},
|
| 53 |
+
"raw_text": "ืืืื ืคืื ื, ืื ืืขื ืืื ืื? ืื ื ืจืืฆื ืืืืคืืก ืืืืจืช ืฉืืจืื ืืกืืชื ืฉืื.",
|
| 54 |
+
"technicalMetadata": null
|
| 55 |
+
},
|
| 56 |
+
"planner_status": "CONSULTATIVE_ACTIVE",
|
| 57 |
+
"planner_guidance": [
|
| 58 |
+
"ืืื ืืชืืืจ ืกืคืจ, ืื ื ืฆืจืื ืืืขืช: ืืื ืขืืืืื ืืฉ ืื? ืื ืืืืื (A4/A5)? ืืืืื ืกืื ืืจืืื (ืจืื/ืงืฉื/ืกืืืืช)?"
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
"test3": {
|
| 62 |
+
"input": "ืื ื ืจืืฆื ืจืืืืค ืงืื, ืืืจ ืขื ืืืจ.",
|
| 63 |
+
"classification": {
|
| 64 |
+
"intent": "quote",
|
| 65 |
+
"answer_text": "ืืื ืชื, ืืชื ืจืืฆื ืจืืืืค ืืืืื ืืืจ ืขื ืืืจ. ืืฆืืื! ืืืื ืกืื ืืืืจ ืืชื ืืขืืืฃ ืืจืืืืค ืฉืื?",
|
| 66 |
+
"products_detected": [
|
| 67 |
+
{
|
| 68 |
+
"product": "rollup",
|
| 69 |
+
"confidence": 1
|
| 70 |
+
}
|
| 71 |
+
],
|
| 72 |
+
"parameters_detected": [
|
| 73 |
+
{
|
| 74 |
+
"key": "size",
|
| 75 |
+
"value": "1x1",
|
| 76 |
+
"context": "rollup",
|
| 77 |
+
"confidence": 0.95
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"_debug": {
|
| 81 |
+
"tokens": {
|
| 82 |
+
"in": 860,
|
| 83 |
+
"out": 147,
|
| 84 |
+
"total": 1007
|
| 85 |
+
},
|
| 86 |
+
"governance": "v5.7-StatefulAdvisor"
|
| 87 |
+
},
|
| 88 |
+
"raw_text": "ืื ื ืจืืฆื ืจืืืืค ืงืื, ืืืจ ืขื ืืืจ.",
|
| 89 |
+
"technicalMetadata": null
|
| 90 |
+
},
|
| 91 |
+
"planner_status": "CONSULTATIVE_ACTIVE",
|
| 92 |
+
"planner_validation": []
|
| 93 |
+
}
|
| 94 |
+
}
|
services/sessionManager.js
CHANGED
|
@@ -22,7 +22,7 @@ function getSession(userId) {
|
|
| 22 |
id: userId,
|
| 23 |
cart: [], // ืืืืฆืจืื ืฉื ืืกืคื ืืขืืื
|
| 24 |
currentProduct: null, // ืืืืฆืจ ืฉืขืืื ืืืืจืื ืืจืืข
|
| 25 |
-
|
| 26 |
processedRequests: new Map(),
|
| 27 |
lastActive: Date.now(),
|
| 28 |
lastSpecChangeTime: Date.now(), // Tracking for Semantic Aging
|
|
@@ -48,7 +48,7 @@ function getSession(userId) {
|
|
| 48 |
|
| 49 |
// Phase 5.3: Semantic Aging (Drift Detection)
|
| 50 |
const driftDelta = (Date.now() - session.lastSpecChangeTime) / 1000 / 60; // in minutes
|
| 51 |
-
if (driftDelta >= 8 && driftDelta <= 12 && session.
|
| 52 |
session.driftDetected = true;
|
| 53 |
console.log(`โณ [SESSION] Semantic Aging detected for ${userId}: ${driftDelta.toFixed(1)}m`);
|
| 54 |
} else {
|
|
@@ -133,7 +133,7 @@ function releaseFailedRequest(session, requestId) {
|
|
| 133 |
function clearSession(userId) {
|
| 134 |
if (sessions[userId]) {
|
| 135 |
sessions[userId].currentProduct = null;
|
| 136 |
-
sessions[userId].
|
| 137 |
sessions[userId].history = [];
|
| 138 |
sessions[userId].lastBotMessage = null;
|
| 139 |
console.log(`๐งน Session context cleared for: ${userId}`);
|
|
@@ -144,7 +144,7 @@ function clearCart(userId) {
|
|
| 144 |
if (sessions[userId]) {
|
| 145 |
sessions[userId].cart = [];
|
| 146 |
sessions[userId].currentProduct = null;
|
| 147 |
-
sessions[userId].
|
| 148 |
sessions[userId].history = [];
|
| 149 |
sessions[userId].lastBotMessage = null;
|
| 150 |
console.log(`๐๏ธ Cart emptied for: ${userId}`);
|
|
|
|
| 22 |
id: userId,
|
| 23 |
cart: [], // ืืืืฆืจืื ืฉื ืืกืคื ืืขืืื
|
| 24 |
currentProduct: null, // ืืืืฆืจ ืฉืขืืื ืืืืจืื ืืจืืข
|
| 25 |
+
active_products: [], // [Spec v5.7.1] ืืืื ืฉืืื ืฉืืืจ ืกืืืืก ืฉื ืืืฆืจืื: draft, priced
|
| 26 |
processedRequests: new Map(),
|
| 27 |
lastActive: Date.now(),
|
| 28 |
lastSpecChangeTime: Date.now(), // Tracking for Semantic Aging
|
|
|
|
| 48 |
|
| 49 |
// Phase 5.3: Semantic Aging (Drift Detection)
|
| 50 |
const driftDelta = (Date.now() - session.lastSpecChangeTime) / 1000 / 60; // in minutes
|
| 51 |
+
if (driftDelta >= 8 && driftDelta <= 12 && session.active_products && session.active_products.length > 0) {
|
| 52 |
session.driftDetected = true;
|
| 53 |
console.log(`โณ [SESSION] Semantic Aging detected for ${userId}: ${driftDelta.toFixed(1)}m`);
|
| 54 |
} else {
|
|
|
|
| 133 |
function clearSession(userId) {
|
| 134 |
if (sessions[userId]) {
|
| 135 |
sessions[userId].currentProduct = null;
|
| 136 |
+
sessions[userId].active_products = [];
|
| 137 |
sessions[userId].history = [];
|
| 138 |
sessions[userId].lastBotMessage = null;
|
| 139 |
console.log(`๐งน Session context cleared for: ${userId}`);
|
|
|
|
| 144 |
if (sessions[userId]) {
|
| 145 |
sessions[userId].cart = [];
|
| 146 |
sessions[userId].currentProduct = null;
|
| 147 |
+
sessions[userId].active_products = [];
|
| 148 |
sessions[userId].history = [];
|
| 149 |
sessions[userId].lastBotMessage = null;
|
| 150 |
console.log(`๐๏ธ Cart emptied for: ${userId}`);
|
tests/test_sanity.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
require('dotenv').config({ path: '../.env' });
|
| 2 |
const { classifyMessage } = require('../engine/classifier');
|
| 3 |
-
const {
|
| 4 |
const fs = require('fs');
|
| 5 |
|
| 6 |
async function runSanityTests() {
|
|
@@ -11,17 +11,17 @@ async function runSanityTests() {
|
|
| 11 |
let intent1 = await classifyMessage(text1, session1);
|
| 12 |
report.test1 = { input: text1, classification: intent1 };
|
| 13 |
|
| 14 |
-
let session2 = { cart: [], currentProduct: null };
|
| 15 |
let text2 = "ืืืื ืคืื ื, ืื ืืขื ืืื ืื? ืื ื ืจืืฆื ืืืืคืืก ืืืืจืช ืฉืืจืื ืืกืืชื ืฉืื.";
|
| 16 |
let intent2 = await classifyMessage(text2, session2);
|
| 17 |
-
let plan2 =
|
| 18 |
-
report.test2 = { input: text2, classification: intent2,
|
| 19 |
|
| 20 |
-
let session3 = { cart: [], currentProduct: '
|
| 21 |
-
let text3 = "ืื ื ืจืืฆื
|
| 22 |
let intent3 = await classifyMessage(text3, session3);
|
| 23 |
-
let plan3 =
|
| 24 |
-
report.test3 = { input: text3, classification: intent3,
|
| 25 |
|
| 26 |
fs.writeFileSync('sanity_results.json', JSON.stringify(report, null, 2), 'utf8');
|
| 27 |
console.log("DONE");
|
|
|
|
| 1 |
require('dotenv').config({ path: '../.env' });
|
| 2 |
const { classifyMessage } = require('../engine/classifier');
|
| 3 |
+
const { compileOrder } = require('../engine/planner');
|
| 4 |
const fs = require('fs');
|
| 5 |
|
| 6 |
async function runSanityTests() {
|
|
|
|
| 11 |
let intent1 = await classifyMessage(text1, session1);
|
| 12 |
report.test1 = { input: text1, classification: intent1 };
|
| 13 |
|
| 14 |
+
let session2 = { cart: [], currentProduct: null, active_products: [] };
|
| 15 |
let text2 = "ืืืื ืคืื ื, ืื ืืขื ืืื ืื? ืื ื ืจืืฆื ืืืืคืืก ืืืืจืช ืฉืืจืื ืืกืืชื ืฉืื.";
|
| 16 |
let intent2 = await classifyMessage(text2, session2);
|
| 17 |
+
let plan2 = compileOrder(intent2, session2);
|
| 18 |
+
report.test2 = { input: text2, classification: intent2, planner_status: plan2.status, planner_guidance: plan2.fallback_guidance };
|
| 19 |
|
| 20 |
+
let session3 = { cart: [], currentProduct: 'rollup', active_products: [{ type: 'rollup', status: 'draft', attributes: { qty: 1 }, history: { qty: [1] } }] };
|
| 21 |
+
let text3 = "ืื ื ืจืืฆื ืจืืืืค ืงืื, ืืืจ ืขื ืืืจ.";
|
| 22 |
let intent3 = await classifyMessage(text3, session3);
|
| 23 |
+
let plan3 = compileOrder(intent3, session3);
|
| 24 |
+
report.test3 = { input: text3, classification: intent3, planner_status: plan3.status, planner_validation: plan3.items };
|
| 25 |
|
| 26 |
fs.writeFileSync('sanity_results.json', JSON.stringify(report, null, 2), 'utf8');
|
| 27 |
console.log("DONE");
|
tests/test_v5_7_state.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config({ path: '../.env' });
|
| 2 |
+
const { classifyMessage } = require('../engine/classifier');
|
| 3 |
+
const { compileOrder, buildSessionStateContext } = require('../engine/planner');
|
| 4 |
+
const { calculate_custom_job } = require('../engine/calculation');
|
| 5 |
+
|
| 6 |
+
async function runTest() {
|
| 7 |
+
console.log("๐ Starting Spec v5.7.1 State Persistence Test...\n");
|
| 8 |
+
|
| 9 |
+
let session = {
|
| 10 |
+
cart: [],
|
| 11 |
+
currentProduct: null,
|
| 12 |
+
active_products: [],
|
| 13 |
+
history: [],
|
| 14 |
+
processedRequests: new Map()
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
// --- TURN 1: Initial Request (Draft State) ---
|
| 18 |
+
console.log("๐ฃ๏ธ User: ืื ื ืจืืฆื ืืืืคืืก ืืืืจืืช ืืจืืื ืืื A4, ืืืืช 300, ืกืืืจืช ืกืืืืช");
|
| 19 |
+
let text1 = "ืื ื ืจืืฆื ืืืืคืืก ืืืืจืืช ืืจืืื ืืื A4, ืืืืช 300, ืกืืืจืช ืกืืืืช";
|
| 20 |
+
let intent1 = await classifyMessage(text1, session);
|
| 21 |
+
let comp1 = compileOrder(intent1, session);
|
| 22 |
+
|
| 23 |
+
console.log("๐ฆ State after Turn 1:");
|
| 24 |
+
console.log(JSON.stringify(session.active_products, null, 2));
|
| 25 |
+
console.log("๐ค System Status:", comp1.status);
|
| 26 |
+
console.log("๐ค System Fallback Guidance:", comp1.fallback_guidance);
|
| 27 |
+
// Should be MISSING pages.
|
| 28 |
+
|
| 29 |
+
console.log("\n------------------------------------------------\n");
|
| 30 |
+
|
| 31 |
+
// --- TURN 2: Provide Missing Parameter (Priced State) ---
|
| 32 |
+
console.log("๐ฃ๏ธ User: 120 ืขืืืืื");
|
| 33 |
+
let text2 = "120 ืขืืืืื";
|
| 34 |
+
let intent2 = await classifyMessage(text2, session);
|
| 35 |
+
let comp2 = compileOrder(intent2, session);
|
| 36 |
+
|
| 37 |
+
// Simulate ChatController transition to cart
|
| 38 |
+
if (comp2.items && comp2.items.length > 0) {
|
| 39 |
+
comp2.items.forEach(item => {
|
| 40 |
+
if (calculate_custom_job) {
|
| 41 |
+
// mock pricing since it might require other deps
|
| 42 |
+
session.cart.push({ product: item.product, ...item.params, price: 500 });
|
| 43 |
+
}
|
| 44 |
+
const activeItem = session.active_products.find(p => p.type === item.product && p.status !== 'confirmed');
|
| 45 |
+
if (activeItem) activeItem.status = 'priced';
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
console.log("๐ Cart:", session.cart.length, "items.");
|
| 50 |
+
console.log("๐ฆ State after Turn 2 (Should be 'priced' but NOT deleted):");
|
| 51 |
+
console.log(JSON.stringify(session.active_products, null, 2));
|
| 52 |
+
|
| 53 |
+
console.log("\n------------------------------------------------\n");
|
| 54 |
+
|
| 55 |
+
// --- TURN 3: Up-sell / Modification on Priced Item ---
|
| 56 |
+
console.log("๐ฃ๏ธ User: ืืขืฆื ืชืฉื ื ืืช ืืืืืช ื-500");
|
| 57 |
+
let text3 = "ืืขืฆื ืชืฉื ื ืืช ืืืืืช ื-500";
|
| 58 |
+
let intent3 = await classifyMessage(text3, session);
|
| 59 |
+
let comp3 = compileOrder(intent3, session);
|
| 60 |
+
|
| 61 |
+
console.log("๐ฆ State after Turn 3 (Checking Amnesia Resistance):");
|
| 62 |
+
console.log(JSON.stringify(session.active_products, null, 2));
|
| 63 |
+
|
| 64 |
+
// If it remembered all attributes, missing should be empty or strictly related to stability, not missing mandatory fields like paper_type.
|
| 65 |
+
let activeBooklet = session.active_products.find(p => p.type === 'booklet');
|
| 66 |
+
|
| 67 |
+
console.log("\n๐งช TEST RESULT:");
|
| 68 |
+
if (activeBooklet && activeBooklet.attributes.paper_type && activeBooklet.attributes.qty == 500 && activeBooklet.attributes.pages) {
|
| 69 |
+
console.log("โ
SUCCESS - LLM retained full context without parsing from UI.");
|
| 70 |
+
} else {
|
| 71 |
+
console.log("โ FAILED - LLM suffered from amnesia.");
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
runTest().catch(console.error);
|