dotandru commited on
Commit
1c434d9
ยท
1 Parent(s): 73b3a3e

V5.7.1: Unified Product State Authority - Fixed post-pricing amnesia & enabled multi-product support

Browse files
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
- delete session.draftAttributes[product];
84
  });
85
  }
86
 
@@ -92,11 +92,14 @@ async function handleChat(req, res) {
92
  ...item.params
93
  });
94
  pushToSessionCart(session, productionItem);
95
- delete session.draftAttributes[item.product];
 
 
 
96
  });
97
 
98
  // C. Status Updates (Only append if LLM didn't explain the status)
99
- const draftProducts = Object.keys(session.draftAttributes || {});
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
- Object.keys(session.draftAttributes || {}).forEach(prodKey => {
 
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: session.draftAttributes[prodKey].params?.qty || 0
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.023361750000000004,
3
- "lastReset": "2026-02-26"
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 draft = session.draftAttributes[product] || { params: {} };
85
  let missing = [];
86
  const template = DOMAIN_TEMPLATES.products[product];
87
  if (template && template.mandatory) {
88
- missing = template.mandatory.filter(p => !draft.params[p]);
89
  }
90
  return {
91
  current_intent: session.lastIntent || "unknown",
92
  active_product: product,
93
- extracted_slots: draft.params,
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 - Stateful Advisor)...");
165
- if (!session.draftAttributes) session.draftAttributes = {};
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
- if (!session.draftAttributes[normProduct]) {
187
- session.draftAttributes[normProduct] = { params: {}, history: {}, status: 'PENDING' };
 
 
 
188
  }
189
- if (item.confidence >= 0.6) session.draftAttributes[normProduct].status = 'EXTRACTED';
 
 
190
  if (isDeleteIntent && (extractedData.raw_text.includes(normProduct) || extractedData.raw_text.includes(DOMAIN_TEMPLATES.products[normProduct]?.label))) {
191
- delete session.draftAttributes[normProduct];
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
- if (!session.draftAttributes[contextKey]) {
203
- session.draftAttributes[contextKey] = { params: {}, history: {}, status: 'PENDING' };
 
 
 
204
  }
205
- const target = session.draftAttributes[contextKey];
206
  const allowedParams = PRODUCT_WHITELIST[contextKey] || [];
207
  if (allowedParams.includes(param.key)) {
208
- if (!target.history[param.key]) target.history[param.key] = [];
209
- target.history[param.key].push(param.value);
210
- target.params[param.key] = param.value;
211
  session.lastSpecChangeTime = Date.now();
212
  }
213
  });
214
 
215
- for (const [product, data] of Object.entries(session.draftAttributes)) {
 
 
216
  let itemUnstable = false;
217
- for (const [key, history] of Object.entries(data.history)) {
218
  const uniqueValues = new Set(history.map(v => String(v)));
219
  if (uniqueValues.size > 1) itemUnstable = true;
220
  }
221
- let finalParams = { ...data.params };
222
- buckets[product] = {
223
- status: data.status === 'EXTRACTED' ? "READY_FOR_INTEGRITY" : "PENDING_CONFIRMATION",
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
- draftAttributes: {}, // ืชืฉื•ื‘ื•ืช ื–ืžื ื™ื•ืช (ืœืคื ื™ ื—ื™ืฉื•ื‘)
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.draftAttributes && Object.keys(session.draftAttributes).length > 0) {
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].draftAttributes = {};
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].draftAttributes = {};
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 { planActions } = require('../engine/planner');
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 = planActions(intent2, session2);
18
- report.test2 = { input: text2, classification: intent2, planner_question: plan2.actions[0].question };
19
 
20
- let session3 = { cart: [], currentProduct: 'alucobond', draftAttributes: { qty: 1 } };
21
- let text3 = "ืื ื™ ืจื•ืฆื” ืฉืœื˜ ืืœื•ืงื•ื‘ื•ื ื“ ืงื˜ืŸ, 10 ืขืœ 10 ืก\"ืž.";
22
  let intent3 = await classifyMessage(text3, session3);
23
- let plan3 = planActions(intent3, session3);
24
- report.test3 = { input: text3, classification: intent3, planner_actions: plan3.actions };
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);