dotandru commited on
Commit
bbb4e4b
ยท
1 Parent(s): e1a8f91
Files changed (4) hide show
  1. engine/classifier.js +73 -145
  2. engine/extractor.js +77 -0
  3. engine/planner.js +103 -0
  4. server.js +220 -164
engine/classifier.js CHANGED
@@ -1,182 +1,110 @@
1
  /**
2
- * Pini Message Classifier V5 (Production Ready)
3
- * =============================================
4
- * ืฉื™ืคื•ืจื™ื:
5
- * 1. ื–ื™ื”ื•ื™ ืฉืืœื•ืช (?) - ืžื•ื ืข ื”ื–ืžื ื” ื‘ื˜ืขื•ืช ื›ืฉืœืงื•ื— ืจืง ืžืชืขื ื™ื™ืŸ.
6
- * 2. ืจื’ื™ืฉื•ืช ืœืžื™ืœื•ืช ืงื™ืฉื•ืจ ("ื›ืžื•", "ื‘ืœื™") ื•ืœืจื’ืฉ ("ืฉื—ื™ื˜ื”", "ืคื™ืฆื•ื™").
7
- * 3. ื˜ื™ืคื•ืœ ื—ื›ื ื‘ืกื˜ื˜ื•ืก ืžื—ื™ืจ.
8
  */
9
 
10
- const PRODUCT_KEYWORDS = {
11
- // ืขื‘ืจื™ืช
12
- 'ืคืœื™ื™ืจ': 'flyer', 'ืคืœื™ื™ืจื™ื': 'flyer', 'ืขืœื•ืŸ': 'flyer',
13
- 'ื›ืจื˜ื™ืก': 'bc', 'ื›ืจื˜ื™ืกื™ื': 'bc', 'ื‘ื™ืงื•ืจ': 'bc', 'ื‘ื™ื–ื ืก': 'bc',
14
- 'ื”ื–ืžื ื”': 'invitation', 'ื”ื–ืžื ื•ืช': 'invitation',
15
- 'ืจื•ืœืืค': 'rollup', 'ื‘ืื ืจ': 'rollup', 'ืฉืžืฉื•ื ื™ืช': 'banner',
16
- 'ืงื ื‘ืก': 'canvas', 'ืชืžื•ื ื”': 'canvas',
17
- 'ืžื“ื‘ืงื”': 'sticker', 'ืžื“ื‘ืงื•ืช': 'sticker', 'ืกื˜ื™ืงืจ': 'sticker',
18
- 'ื—ื•ื‘ืจืช': 'booklet', 'ืงื˜ืœื•ื’': 'booklet', 'ืžื—ื‘ืจืช': 'booklet', 'ืกืคืจ': 'booklet',
19
-
20
- // ืื ื’ืœื™ืช
21
- 'flyer': 'flyer', 'flyers': 'flyer',
22
- 'business card': 'bc', 'cards': 'bc',
23
- 'invitation': 'invitation', 'invites': 'invitation',
24
- 'sticker': 'sticker', 'stickers': 'sticker',
25
- 'rollup': 'rollup', 'banner': 'rollup'
26
- };
27
 
28
- const ACTION_KEYWORDS = {
29
- remove: ['ืชืžื—ืง', 'ื”ืกืจ', 'ืชื•ืจื™ื“', 'ื‘ื˜ืœ', 'ื”ื•ืฆื', 'ืœื ืฆืจื™ืš', 'remove', 'delete'],
30
- update: ['ืฉื ื”', 'ืขื“ื›ืŸ', 'ืชื—ืœื™ืฃ', 'ื‘ืžืงื•ื', 'ืชืขืœื” ืœ', 'ืชื•ืจื™ื“ ืœ', 'change', 'update'],
31
- clear: ['ื ืงื” ื”ื›ืœ', 'ืชืžื—ืง ื”ื›ืœ', 'ืžื—ืง ืขื’ืœื”', 'ื”ืชื—ืœ ืžื—ื“ืฉ', 'ืื™ืคื•ืก', 'ืขื–ื•ื‘ ื”ื›ืœ', 'reset', 'clear'],
32
- greeting: ['ื”ื™ื™', 'ืฉืœื•ื', 'ื‘ื•ืงืจ ื˜ื•ื‘', 'ืขืจื‘ ื˜ื•ื‘', 'ืžื” ืงื•ืจื”', 'ืžื” ื ืฉืžืข', 'ืื”ืœืŸ', 'hi', 'hello'],
33
- send_quote: ['ืฉืœื—', 'ื”ืฆืขื”', 'ืกื’ื•ืจ', 'ืชืฉืœื—', 'send', 'quote', 'finish', 'ื—ืฉื‘ื•ื ื™ืช'],
34
- status: ['ื›ืžื” ื–ื”', 'ืžื—ื™ืจ', 'ืกื™ื›ื•ื', 'ืขื’ืœื”', 'ืชืจืื” ืœื™', 'ืกื”"ื›', 'ื›ืžื” ื™ื•ืฆื', 'total', 'status']
 
35
  };
36
 
37
- const HEBREW_NUMBERS = {
38
- 'ืื—ื“': 1, 'ืื—ืช': 1, 'ืฉื ื™': 2, 'ืฉืชื™': 2, 'ืฉื ื™ื™ื': 2, 'ืฉืœื•ืฉื”': 3, 'ืฉืœื•ืฉ': 3,
39
- 'ืืจื‘ืขื”': 4, 'ืืจื‘ืข': 4, 'ื—ืžื™ืฉื”': 5, 'ื—ืžืฉ': 5, 'ืขืฉืจื”': 10, 'ืขืฉืจ': 10,
40
- 'ืžืื”': 100, 'ืžืืชื™ื™ื': 200, 'ืืœืฃ': 1000, 'ืืœืคื™ื™ื': 2000
41
- };
42
-
43
- // ๐Ÿšฉ ืžื™ืœื™ื ืฉืžืงืคื™ืฆื•ืช ืœ-LLM ืžื™ื“
44
- const COMPLEXITY_TRIGGERS = [
45
- 'ืœืžื”', 'ืื™ืš', 'ืžืชื™', 'ื”ืื', 'ืชืœื•ื™',
46
- 'ืงื•ื‘ืฅ', 'ื“ื—ื•ืฃ', 'ื”ื ื—ื”', 'ื™ืงืจ', 'ื–ื•ืœ', 'ืžืฉืœื•ื—', 'ื”ื‘ื“ืœ',
47
- 'ืคื™ืฆื•ื™', 'ื—ื™ื ื', 'ืชืœื•ื ื”', 'ืฉื—ื™ื˜ื”', 'ื’ืจื•ืข', 'ืขืงื•ื', // ืจื’ืฉ ืฉืœื™ืœื™/ืฉื™ืจื•ืช
48
  'ื›ืžื•', 'ื‘ืขืจืš', 'ืื•ืœื™', // ืื™ ื•ื“ืื•ืช
49
- 'ื‘ืœื™ื“', 'ืงืจื•ืค', 'ื•ืงื˜ื•ืจ', // ืžื•ื ื—ื™ื ื˜ื›ื ื™ื™ื
50
- 'ืœืžื—ืจ', 'ื”ื™ื•ื', // ื“ื—ื™ืคื•ืช
51
- 'why', 'how', 'when', 'discount', 'expensive'
52
  ];
53
 
54
- /**
55
- * ๐Ÿง  ื–ื™ื”ื•ื™ ืžื•ืจื›ื‘ื•ืช ื—ื›ื
56
- */
57
- function isComplexOrder(text) {
58
- // 1. ืกื™ืžื ื™ ื—ื™ื‘ื•ืจ ืžืคื•ืจืฉื™ื
59
- if (text.includes('+') || text.includes(' plus ')) return true;
60
-
61
- // 2. ืจื™ื‘ื•ื™ ืžื•ืฆืจื™ื (ืœืžืฉืœ: "ืคืœื™ื™ืจ ื•ื’ื ื›ืจื˜ื™ืก")
62
- const foundCategories = new Set();
63
- for (const [keyword, category] of Object.entries(PRODUCT_KEYWORDS)) {
64
- if (text.includes(keyword)) {
65
- foundCategories.add(category);
66
- }
67
- }
68
-
69
- if (foundCategories.size > 1) return true;
70
-
71
- return false;
72
- }
73
-
74
- function classifyMessage(message, context = {}) {
75
- let text = message.toLowerCase();
76
  const cart = context.cart || [];
 
77
 
78
- // ื”ืžืจืช ืžืกืคืจื™ื ื‘ืขื‘ืจื™ืช
79
- for (const [word, num] of Object.entries(HEBREW_NUMBERS)) {
80
- if (text.includes(word)) {
81
- text = text.replace(word, num);
82
- }
83
  }
84
 
85
- // --- ืฉืœื‘ 0: Safety & Complexity ---
86
 
87
- // ื. ื”ื–ืžื ื” ืžืจื•ื‘ืช ืžื•ืฆืจื™ื -> LLM
88
- if (isComplexOrder(text)) {
89
- return { action: 'chat', data: {}, needsLLM: true };
90
- }
91
-
92
- // ื‘. ื˜ืจื™ื’ืจื™ื ืžื™ืœื•ืœื™ื™ื (ืจื’ืฉ, ื“ื—ื™ืคื•ืช, ื˜ื›ื ื™) -> LLM
93
- if (COMPLEXITY_TRIGGERS.some(t => text.includes(t))) {
94
- return { action: 'chat', data: {}, needsLLM: true };
95
- }
96
-
97
- // ื’. ื–ื™ื”ื•ื™ ืฉืืœื•ืช ืขืœ ืžื•ืฆืจื™ื ("ื”ืงื ื‘ืกื™ื ื–ื” ืขื ืžืกื’ืจืช?") -> LLM
98
- // ืื ื™ืฉ ืฉื ืฉืœ ืžื•ืฆืจ ื•ื’ื ืกื™ืžืŸ ืฉืืœื”, ื•ื–ื• ืœื ืฉืืœืช ืžื—ื™ืจ ืกื˜ื ื“ืจื˜ื™ืช
99
- const hasProduct = findProductInText(text);
100
- const isPriceQuestion = text.includes('ื›ืžื”') || text.includes('ืžื—ื™ืจ') || text.includes('cost');
101
- if (hasProduct && text.includes('?') && !isPriceQuestion) {
102
- return { action: 'chat', data: {}, needsLLM: true };
103
- }
104
-
105
- // --- ืฉืœื‘ 1: ื–ื™ื”ื•ื™ ืœื™ื‘ื” (ืžื•ืฆืจ + ื›ืžื•ืช) ---
106
- const qtyMatch = text.match(/(\d{1,3}(?:,\d{3})*)/);
107
- const qty = qtyMatch ? parseInt(qtyMatch[0].replace(/,/g, '')) : null;
108
- const product = findProductInText(text);
109
-
110
- if (qty && product) {
111
- // ื”ืื ื–ื” ืขื“ื›ื•ืŸ?
112
- const isUpdate = ACTION_KEYWORDS.update.some(k => text.includes(k)) ||
113
- (cart.some(i => i.product_name === product) &&
114
- !text.includes('ืขื•ื“') && !text.includes('ืชื•ืกื™ืฃ') && !text.includes('ื’ื'));
115
-
116
- // ืชื™ืงื•ืŸ: ืื ื”ืœืงื•ื— ืื•ืžืจ "ืชื•ืจื™ื“ ืœ-2000" ื–ื” Update, ืื‘ืœ ืื ื”ื•ื ืื•ืžืจ "ื–ื” ื™ืงืจ, ืชื•ืจื™ื“" - ื”-Complex Trigger ื›ื‘ืจ ืชืคืก ืืช ื–ื” ืœืžืขืœื”
117
- if (isUpdate && (text.includes('ืฉื ื”') || text.includes('ืขื“ื›ืŸ') || text.includes('ื‘ืžืงื•ื') || text.includes('ืชื•ืจื™ื“') || text.includes('ืชืขืœื”'))) {
118
- return { action: 'update_qty', data: { product, qty }, needsLLM: false };
119
- }
120
- return { action: 'quote', data: { product, qty }, needsLLM: false };
121
  }
122
 
123
- // --- ืฉืœื‘ 2: ืคืขื•ืœื•ืช ืคืฉื•ื˜ื•ืช ---
124
-
125
- // ื‘ืจื›ื•ืช
126
- if (ACTION_KEYWORDS.greeting.some(k => text.includes(k)) && text.length < 20 && !text.includes('?')) {
127
- return { action: 'greeting', data: {}, needsLLM: false };
128
  }
129
 
130
- // ื ื™ืงื•ื™
131
- if (ACTION_KEYWORDS.clear.some(k => text.includes(k))) {
132
- return { action: 'clear', data: {}, needsLLM: false };
133
  }
134
 
135
- // ืฉืœื™ื—ื” / ืกื™ื•ื
136
- if (ACTION_KEYWORDS.send_quote.some(k => text.includes(k))) {
137
- return { action: 'send_quote', data: {}, needsLLM: false };
138
  }
139
 
140
- // ืกื˜ื˜ื•ืก (ืจืง ืื ืœื ื ืชืคืก ืœืžืขืœื” ืข"ื™ ื˜ืจื™ื’ืจื™ื ื›ืžื• "ืฉื—ื™ื˜ื”")
141
- if (ACTION_KEYWORDS.status.some(k => text.includes(k))) {
142
- return { action: 'status', data: {}, needsLLM: false };
143
- }
144
 
145
- // ืขื™ืฆื•ื‘ / ื‘ื“ื™ืงืช ืงื‘ืฆื™ื
146
- if ((text.includes('ืขื™ืฆื•ื‘') || text.includes('pdf') || text.includes('ืงื•ื‘ืฅ')) &&
147
- (text.includes('ื™ืฉ') || text.includes('ืžื•ื›ืŸ') || text.includes('ืžืžื ื™') || text.includes('ื‘ื“ื™ืงื”'))) {
148
- return { action: 'design_check', data: {}, needsLLM: false };
149
- }
 
 
 
 
 
150
 
151
- // ื”ืกืจื”
152
- if (ACTION_KEYWORDS.remove.some(k => text.includes(k))) {
153
- const productToRemove = findProductInText(text);
154
- return { action: 'remove', data: { product: productToRemove }, needsLLM: false };
155
  }
156
 
157
- // --- ืฉืœื‘ 3: ื”ืฉืœืžื•ืช ---
158
-
159
- if (product && !qty) {
160
- return { action: 'quote_incomplete', data: { product }, needsLLM: false };
161
  }
162
 
163
- if (qty && !product) {
164
- if (cart.length > 0) {
165
- return { action: 'update_qty', data: { qty }, needsLLM: false };
166
- }
167
  }
168
 
169
- // ื‘ืจื™ืจืช ืžื—ื“ืœ
170
- return { action: 'chat', data: {}, needsLLM: true };
171
  }
172
 
173
- function findProductInText(text) {
174
- for (const [keyword, key] of Object.entries(PRODUCT_KEYWORDS)) {
175
- if (text.includes(keyword)) {
176
- return key;
177
- }
178
  }
179
  return null;
180
  }
181
 
 
 
 
 
 
 
182
  module.exports = { classifyMessage };
 
1
  /**
2
+ * Pini Classifier V2 (Rule-Based First)
3
+ * =====================================
4
+ * ืžืกื•ื•ื’ ื”ื•ื“ืขื•ืช ืขืœ ื‘ืกื™ืก ื—ื•ืงื™ื ืงืฉื™ื—ื™ื.
5
+ * ื”ืžื˜ืจื”: 90% ืžื”ื”ื•ื“ืขื•ืช ืœื ืฆืจื™ื›ื•ืช ืœื”ื’ื™ืข ืœ-LLM.
 
 
6
  */
7
 
8
+ const { PRODUCT_MAP } = require('./calculation');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ // ืžื™ืœื•ืช ืžืคืชื— ืœืคืขื•ืœื•ืช
11
+ const ACTIONS = {
12
+ REMOVE: ['ืชืžื—ืง', 'ื”ืกืจ', 'ืชื•ืจื™ื“', 'ื‘ื˜ืœ', 'ื”ื•ืฆื', 'ืœื ืฆืจื™ืš', 'remove', 'delete', 'cancel'],
13
+ UPDATE: ['ืฉื ื”', 'ืขื“ื›ืŸ', 'ืชื—ืœื™ืฃ', 'ื‘ืžืงื•ื', 'ืชืขืœื” ืœ', 'ืชื•ืจื™ื“ ืœ', 'change', 'update', 'edit'],
14
+ CLEAR: ['ื ืงื” ื”ื›ืœ', 'ืชืžื—ืง ื”ื›ืœ', 'ืžื—ืง ืขื’ืœื”', 'ื”ืชื—ืœ ืžื—ื“ืฉ', 'ืื™ืคื•ืก', 'ืขื–ื•ื‘ ื”ื›ืœ', 'reset', 'clear'],
15
+ STATUS: ['ื›ืžื” ื–ื”', 'ืžื—ื™ืจ', 'ืกื™ื›ื•ื', 'ืขื’ืœื”', 'ืชืจืื” ืœื™', 'ืกื”"ื›', 'ื›ืžื” ื™ื•ืฆื', 'status', 'total'],
16
+ SEND: ['ืฉืœื—', 'ื”ืฆืขื”', 'ืกื’ื•ืจ', 'ืชืฉืœื—', 'ื—ืฉื‘ื•ื ื™ืช', 'ืชื›ื™ืŸ ืœื™', 'send', 'finish', 'checkout'],
17
+ GREETING: ['ื”ื™ื™', 'ืฉืœื•ื', 'ื‘ื•ืงืจ ื˜ื•ื‘', 'ืขืจื‘ ื˜ื•ื‘', 'ืคื™ื ื™', 'ืื”ืœืŸ', 'hi', 'hello']
18
  };
19
 
20
+ // ื˜ืจื™ื’ืจื™ื ืœืžื•ืจื›ื‘ื•ืช (ืžื—ื™ื™ื‘ื™ื LLM)
21
+ const COMPLEX_TRIGGERS = [
22
+ 'ืœืžื”', 'ืื™ืš', 'ืžืชื™', 'ื”ืื', 'ืชืœื•ื™', 'ื”ื‘ื“ืœ',
23
+ 'ืคื™ืฆื•ื™', 'ื—ื™ื ื', 'ืชืœื•ื ื”', 'ืฉื—ื™ื˜ื”', 'ื’ืจื•ืข', // ืจื’ืฉ ืฉืœื™ืœื™
 
 
 
 
 
 
 
24
  'ื›ืžื•', 'ื‘ืขืจืš', 'ืื•ืœื™', // ืื™ ื•ื“ืื•ืช
25
+ 'ืขื™ืฆื•ื‘', 'ื’ืจืคื™ืงื”', 'ืœื•ื’ื•', // ืขื™ืฆื•ื‘ (ื“ื•ืจืฉ ื”ื‘ื ื”)
26
+ 'why', 'how', 'when', 'difference'
 
27
  ];
28
 
29
+ function classifyMessage(text, context = {}) {
30
+ const cleanText = text.toLowerCase().trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  const cart = context.cart || [];
32
+ const hasCart = cart.length > 0;
33
 
34
+ // 1. ื‘ื“ื™ืงืช ื‘ื˜ื™ื—ื•ืช: ื”ืื ื–ื• ื‘ืงืฉื” ืžื•ืจื›ื‘ืช?
35
+ if (COMPLEX_TRIGGERS.some(t => cleanText.includes(t))) {
36
+ return { intent: 'consult', confidence: 1.0, needsLLM: true, reason: 'complexity_trigger' };
 
 
37
  }
38
 
39
+ // 2. ื–ื™ื”ื•ื™ ืคืขื•ืœื•ืช ื‘ืจื•ืจื•ืช (Keywords)
40
 
41
+ // ื ื™ืงื•ื™
42
+ if (ACTIONS.CLEAR.some(k => cleanText.includes(k))) {
43
+ return { intent: 'clear', confidence: 1.0, needsLLM: false };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
+ // ื”ืกืจื” (ืจืง ืื ื™ืฉ ืžื•ืฆืจ ื‘ืžืฉืคื˜)
47
+ if (ACTIONS.REMOVE.some(k => cleanText.includes(k))) {
48
+ return { intent: 'remove', confidence: 0.9, needsLLM: false };
 
 
49
  }
50
 
51
+ // ืกื™ื•ื / ืฉืœื™ื—ื”
52
+ if (ACTIONS.SEND.some(k => cleanText.includes(k))) {
53
+ return { intent: 'checkout', confidence: 1.0, needsLLM: false };
54
  }
55
 
56
+ // ืกื˜ื˜ื•ืก
57
+ if (ACTIONS.STATUS.some(k => cleanText.includes(k))) {
58
+ return { intent: 'status', confidence: 1.0, needsLLM: false };
59
  }
60
 
61
+ // 3. ื–ื™ื”ื•ื™ ื”ื–ืžื ื”/ืขื“ื›ื•ืŸ (ืžืกืคืจ + ืžื•ืฆืจ)
62
+ const hasNumber = /\d+/.test(cleanText) || containsHebrewNumber(cleanText);
63
+ const productKey = identifyProductInText(cleanText);
 
64
 
65
+ if (hasNumber) {
66
+ // ืื ื™ืฉ ืžื™ืœืช ืขื“ื›ื•ืŸ ("ืฉื ื” ืœ-1000")
67
+ if (ACTIONS.UPDATE.some(k => cleanText.includes(k))) {
68
+ return { intent: 'update', confidence: 0.9, needsLLM: false };
69
+ }
70
+
71
+ // ืื ื™ืฉ ืžื•ืฆืจ ืžืคื•ืจืฉ ("1000 ืคืœื™ื™ืจื™ื")
72
+ if (productKey) {
73
+ return { intent: 'quote', confidence: 1.0, needsLLM: false }; // ื”ื•ืกืคื” ื—ื“ืฉื”
74
+ }
75
 
76
+ // ืื ื™ืฉ ืจืง ืžืกืคืจ ("1000") ื•ื™ืฉ ืžืฉื”ื• ื‘ืขื’ืœื” -> ืขื“ื›ื•ืŸ ืื—ืจื•ืŸ
77
+ if (!productKey && hasCart) {
78
+ return { intent: 'update', confidence: 0.8, needsLLM: false };
79
+ }
80
  }
81
 
82
+ // 4. ืžื•ืฆืจ ืœืœื ื›ืžื•ืช ("ืื ื™ ืฆืจื™ืš ืคืœื™ื™ืจื™ื")
83
+ if (productKey && !hasNumber) {
84
+ return { intent: 'quote', confidence: 0.9, needsLLM: false }; // ื™ื˜ื•ืคืœ ื›-Missing Info
 
85
  }
86
 
87
+ // 5. ื‘ืจื›ื” (ืจืง ืื ืงืฆืจ)
88
+ if (ACTIONS.GREETING.some(k => cleanText.includes(k)) && cleanText.length < 20) {
89
+ return { intent: 'greeting', confidence: 0.9, needsLLM: false };
 
90
  }
91
 
92
+ // ื‘ืจื™ืจืช ืžื—ื“ืœ: ืœื ื”ื‘ื ื• -> LLM
93
+ return { intent: 'consult', confidence: 0.5, needsLLM: true, reason: 'unknown' };
94
  }
95
 
96
+ // ืขื–ืจ: ื–ื™ื”ื•ื™ ืžื•ืฆืจ ื‘ื˜ืงืกื˜
97
+ function identifyProductInText(text) {
98
+ for (const [keyword, category] of Object.entries(PRODUCT_MAP)) {
99
+ if (text.includes(keyword)) return category;
 
100
  }
101
  return null;
102
  }
103
 
104
+ // ืขื–ืจ: ื–ื™ื”ื•ื™ ืžืกืคืจ ื‘ืขื‘ืจื™ืช
105
+ function containsHebrewNumber(text) {
106
+ const hebrewNumbers = ['ืื—ื“', 'ืฉืชื™ื™ื', 'ืฉืœื•ืฉ', 'ืืจื‘ืข', 'ื—ืžืฉ', 'ืฉืฉ', 'ืฉื‘ืข', 'ืฉืžื•ื ื”', 'ืชืฉืข', 'ืขืฉืจ', 'ืžืื”', 'ืืœืฃ'];
107
+ return hebrewNumbers.some(n => text.includes(n));
108
+ }
109
+
110
  module.exports = { classifyMessage };
engine/extractor.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Parameter Extractor
3
+ * ===================
4
+ * ืžื—ืœืฅ ื ืชื•ื ื™ื ืžื•ื‘ื ื™ื ืžื˜ืงืกื˜ ื—ื•ืคืฉื™ ืœืœื ืฉื™ืžื•ืฉ ื‘-AI.
5
+ * ืชื•ืžืš ื‘ืžืกืคืจื™ื, ื›ืžื•ื™ื•ืช (k), ื•ืžื•ืฆืจื™ื.
6
+ */
7
+
8
+ const { PRODUCT_MAP } = require('./calculation');
9
+
10
+ const HEBREW_NUMBERS = {
11
+ 'ืื—ื“': 1, 'ืื—ืช': 1, 'ืฉื ื™': 2, 'ืฉืชื™': 2, 'ืฉื ื™ื™ื': 2, 'ืฉืœื•ืฉื”': 3, 'ืฉืœื•ืฉ': 3,
12
+ 'ืืจื‘ืขื”': 4, 'ืืจื‘ืข': 4, 'ื—ืžื™ืฉื”': 5, 'ื—ืžืฉ': 5, 'ืฉื™ืฉื”': 6, 'ืฉืฉ': 6,
13
+ 'ืฉื‘ืขื”': 7, 'ืฉื‘ืข': 7, 'ืฉืžื•ื ื”': 8, 'ืชืฉืขื”': 9, 'ืชืฉืข': 9, 'ืขืฉืจื”': 10, 'ืขืฉืจ': 10,
14
+ 'ืขืฉืจื™ื': 20, 'ื—ืžื™ืฉื™ื': 50, 'ืžืื”': 100, 'ืžืืชื™ื™ื': 200, 'ื—ืžืฉ ืžืื•ืช': 500,
15
+ 'ืืœืฃ': 1000, 'ืืœืคื™ื™ื': 2000, 'ื—ืžืฉืช ืืœืคื™ื': 5000, 'ืขืฉืจืช ืืœืคื™ื': 10000
16
+ };
17
+
18
+ const MODIFIERS = {
19
+ 'ื“ื—ื•ืฃ': { urgency: 'high' },
20
+ 'ืžื”ืจ': { urgency: 'high' },
21
+ 'ื”ื™ื•ื': { urgency: 'high' },
22
+ 'ืžื—ืจ': { urgency: 'high' },
23
+ 'ืืงืกืคืจืก': { urgency: 'high' },
24
+ 'ืขื›ืฉื™ื•': { urgency: 'high' },
25
+ 'ื–ื•ืœ': { budget: 'low' },
26
+ 'ื”ื›ื™ ื˜ื•ื‘': { quality: 'high' },
27
+ 'ืคืจื™ืžื™ื•ื': { quality: 'high' }
28
+ };
29
+
30
+ function extractParameters(text) {
31
+ let cleanText = text.toLowerCase().replace(/,/g, ''); // ื”ืกืจืช ืคืกื™ืงื™ื (1,000 -> 1000)
32
+ const params = {
33
+ product: null,
34
+ qty: null,
35
+ attributes: {}
36
+ };
37
+
38
+ // 1. ื—ื™ืœื•ืฅ ืžื•ืฆืจ
39
+ for (const [keyword, category] of Object.entries(PRODUCT_MAP)) {
40
+ if (cleanText.includes(keyword)) {
41
+ params.product = category;
42
+ break; // ืžืกืคื™ืง ืžื•ืฆืจ ืื—ื“ ืœืžืฉืคื˜ ืคืฉื•ื˜
43
+ }
44
+ }
45
+
46
+ // 2. ื—ื™ืœื•ืฅ ื›ืžื•ืช (ืžืกืคืจื™ื)
47
+ // ืชืžื™ื›ื” ื‘-k (1k = 1000)
48
+ const kMatch = cleanText.match(/(\d+)k/);
49
+ if (kMatch) {
50
+ params.qty = parseInt(kMatch[1]) * 1000;
51
+ } else {
52
+ // ืžืกืคืจ ืจื’ื™ืœ
53
+ const numMatch = cleanText.match(/\d+/);
54
+ if (numMatch) {
55
+ params.qty = parseInt(numMatch[0]);
56
+ } else {
57
+ // ืžืกืคืจ ื‘ืžื™ืœื™ื
58
+ for (const [word, val] of Object.entries(HEBREW_NUMBERS)) {
59
+ if (cleanText.includes(word)) {
60
+ params.qty = val;
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // 3. ื—ื™ืœื•ืฅ ืชื›ื•ื ื•ืช ื ื•ืกืคื•ืช (ื“ื—ื™ืคื•ืช, ืื™ื›ื•ืช)
68
+ for (const [word, attr] of Object.entries(MODIFIERS)) {
69
+ if (cleanText.includes(word)) {
70
+ Object.assign(params.attributes, attr);
71
+ }
72
+ }
73
+
74
+ return params;
75
+ }
76
+
77
+ module.exports = { extractParameters };
engine/planner.js ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Pini Planner (The Brain)
3
+ * ========================
4
+ * ืžืงื‘ืœ ื›ื•ื•ื ื” ื•ืคืจืžื˜ืจื™ื -> ืžื—ื–ื™ืจ ืจืฉื™ืžืช ืžืฉื™ืžื•ืช ืœื‘ื™ืฆื•ืข.
5
+ * ื–ื” ื”ืžืงื•ื ืฉื‘ื• ื ืงื‘ืขืช ื”"ืืกื˜ืจื˜ื’ื™ื”".
6
+ */
7
+
8
+ function planActions(intent, params, session) {
9
+ const plan = {
10
+ actions: [],
11
+ nextState: 'idle' // ืœืžืขืงื‘ ืื—ืจื™ ื”ืกื˜ื™ื™ื˜ ื‘ืฉื™ื—ื”
12
+ };
13
+
14
+ // === ืœื•ื’ื™ืงืช ืชื›ื ื•ืŸ ืœืคื™ ื›ื•ื•ื ื” ===
15
+
16
+ switch (intent) {
17
+ case 'quote':
18
+ // ื”ืื ื™ืฉ ืืช ื›ืœ ื”ืžื™ื“ืข?
19
+ if (params.product && params.qty) {
20
+ // ื™ืฉ ืžื•ืฆืจ ื•ื›ืžื•ืช -> ื”ื•ืกืฃ ืœืขื’ืœื”
21
+ plan.actions.push({
22
+ type: 'CALCULATE_AND_ADD',
23
+ payload: { product: params.product, qty: params.qty }
24
+ });
25
+
26
+ // ื”ืื ื‘ื™ืงืฉ ื“ื—ื•ืฃ? -> ื”ื•ืกืฃ ื‘ื“ื™ืงืช ื“ื—ื™ืคื•ืช
27
+ if (params.attributes.urgency === 'high') {
28
+ plan.actions.push({ type: 'CHECK_URGENCY_OPTIONS' });
29
+ }
30
+
31
+ // ื‘ืกื•ืฃ -> ื”ืฆื’ ืชื•ืฆืื”
32
+ plan.actions.push({ type: 'GENERATE_RESPONSE', template: 'quote_added' });
33
+
34
+ // *** ืขื“ื›ื•ืŸ ื“ืฉื‘ื•ืจื“ ื—ื•ื‘ื”! ***
35
+ plan.actions.push({ type: 'UPDATE_DASHBOARD' });
36
+
37
+ } else if (params.product && !params.qty) {
38
+ // ื—ืกืจื” ื›ืžื•ืช -> ืฉืืœ ืืช ื”ืœืงื•ื—
39
+ plan.actions.push({
40
+ type: 'ASK_QUESTION',
41
+ question: 'quantity',
42
+ product: params.product
43
+ });
44
+ }
45
+ break;
46
+
47
+ case 'update':
48
+ // ืขื“ื›ื•ืŸ ืคืจื™ื˜ ืงื™ื™ื
49
+ const targetProduct = params.product || getLastAddedProduct(session);
50
+ if (targetProduct && params.qty) {
51
+ plan.actions.push({
52
+ type: 'UPDATE_CART_ITEM',
53
+ payload: { product: targetProduct, qty: params.qty }
54
+ });
55
+ plan.actions.push({ type: 'GENERATE_RESPONSE', template: 'quote_updated' });
56
+ plan.actions.push({ type: 'UPDATE_DASHBOARD' });
57
+ } else {
58
+ plan.actions.push({ type: 'ASK_CLARIFICATION', context: 'update_what' });
59
+ }
60
+ break;
61
+
62
+ case 'remove':
63
+ const productToRemove = params.product || getLastAddedProduct(session);
64
+ if (productToRemove) {
65
+ plan.actions.push({ type: 'REMOVE_FROM_CART', product: productToRemove });
66
+ plan.actions.push({ type: 'GENERATE_RESPONSE', template: 'item_removed' });
67
+ plan.actions.push({ type: 'UPDATE_DASHBOARD' });
68
+ }
69
+ break;
70
+
71
+ case 'checkout':
72
+ plan.actions.push({ type: 'SUMMARIZE_CART' });
73
+ plan.actions.push({ type: 'CHECK_DESIGN_STATUS' }); // ื‘ื“ื™ืงื” ื—ื›ืžื” ืœืคื ื™ ืกื’ื™ืจื”
74
+ plan.actions.push({ type: 'GENERATE_RESPONSE', template: 'send_quote' });
75
+ break;
76
+
77
+ case 'clear':
78
+ plan.actions.push({ type: 'CLEAR_CART' });
79
+ plan.actions.push({ type: 'GENERATE_RESPONSE', template: 'cart_cleared' });
80
+ plan.actions.push({ type: 'UPDATE_DASHBOARD' });
81
+ break;
82
+
83
+ case 'greeting':
84
+ plan.actions.push({ type: 'GENERATE_RESPONSE', template: 'greeting' });
85
+ break;
86
+
87
+ case 'consult':
88
+ default:
89
+ // ื”ืขื‘ืจื” ืœ-LLM (ืจืง ื›ืฉืื™ืŸ ื‘ืจื™ืจื”)
90
+ plan.actions.push({ type: 'CALL_LLM_CONSULTANT', input: params });
91
+ break;
92
+ }
93
+
94
+ return plan;
95
+ }
96
+
97
+ // ืขื–ืจ: ืฉืœื™ืคืช ื”ืžื•ืฆืจ ื”ืื—ืจื•ืŸ ืžื”ืกืฉืŸ
98
+ function getLastAddedProduct(session) {
99
+ if (!session.cart || session.cart.length === 0) return null;
100
+ return session.cart[session.cart.length - 1].product_category; // ืื• product_name
101
+ }
102
+
103
+ module.exports = { planActions };
server.js CHANGED
@@ -1,25 +1,30 @@
1
  /**
2
- * Pini Print Bot - Server V7 (Strategy Enabled)
3
- * =============================================
4
- * ืฉืจืช ื—ื›ื ืฉื™ื•ื“ืข ืœื ืจืง ืœื”ื’ื™ื‘ ืืœื ื’ื ืœื™ื–ื•ื ืžื”ืœื›ื™ื (Proactive)
 
5
  */
6
 
7
  const express = require('express');
8
  const cors = require('cors');
9
  const dotenv = require('dotenv');
10
- const { GoogleGenerativeAI } = require('@google/generative-ai');
 
11
 
12
- // ื™ื™ื‘ื•ื ืžื ื•ืขื™ื
13
- const { classifyMessage } = require('./engine/classifier');
14
- const { routeRequest } = require('./engine/llmRouter');
 
15
  const { calculate_custom_job } = require('./engine/calculation');
16
- const { getSession, removeFromCart, clearCart, addToHistory } = require('./services/sessionManager');
17
- const { generateQuotePDF } = require('./services/pdfService');
18
- const { buildResponse, buildQuickReplies } = require('./engine/responseBuilder');
19
  const { generateDashboard } = require('./engine/dashboardManager');
 
 
20
  const { findOrCreateCustomer, extractPhoneFromText, extractNameFromText } = require('./engine/customerManager');
21
- const { PRODUCT_CATALOG } = require('./engine/productCatalog');
22
- const { detectTaskType, buildPrompt, validateResponse, fixResponse } = require('./engine/smartLLM');
 
 
 
23
 
24
  dotenv.config();
25
  const app = express();
@@ -28,195 +33,241 @@ app.use(cors({ origin: '*' }));
28
  app.use(express.json());
29
  app.use(express.static('public'));
30
 
31
- const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
32
- const chatModel = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
33
-
34
- // ื”ื’ื“ืจื•ืช ื‘ืจื™ืจืช ืžื—ื“ืœ ืœืืกื˜ืจื˜ื’ื™ื”
35
- const POPULAR_DEFAULTS = {
36
- 'bc': 500,
37
- 'flyer': 1000,
38
- 'invitation': 200,
39
- 'rollup': 1,
40
- 'sticker': 500,
41
- 'booklet': 50
42
  };
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  app.post('/api/chat', async (req, res) => {
 
45
  try {
46
  const { message, userId, phone, customerName } = req.body;
47
  if (!message) return res.status(400).json({ error: 'Missing message' });
48
-
49
- console.log(`\n๐Ÿ’ฌ User: "${message}"`);
 
 
 
50
  const session = getSession(userId);
 
51
 
52
- // ื–ื™ื”ื•ื™ ืœืงื•ื—
53
  const extractedPhone = extractPhoneFromText(message);
54
  if (phone || extractedPhone) {
55
- findOrCreateCustomer(phone || extractedPhone, customerName || extractNameFromText(message));
56
- session.customerPhone = phone || extractedPhone;
 
57
  }
58
 
59
- // ื ื™๏ฟฝ๏ฟฝื•ื‘
60
- let routerResult;
61
- const classification = classifyMessage(message, { cart: session.cart });
62
 
 
 
 
 
 
 
 
 
 
63
  if (!classification.needsLLM) {
64
- console.log(`โšก Fast Path: ${classification.action}`);
65
- routerResult = {
66
- intent: mapActionToIntent(classification.action),
67
- strategy: 'standard', // ืžืกืœื•ืœ ืžื”ื™ืจ ื”ื•ื ืชืžื™ื“ ืกื˜ื ื“ืจื˜ื™
68
- items: [{ product: classification.data?.product, qty: classification.data?.qty }]
69
- };
70
  } else {
71
- console.log(`๐Ÿค– Using LLM (Strategy Engine)...`);
72
- routerResult = await routeRequest(message, session.cart, session.history);
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
 
 
 
 
 
 
 
74
 
75
- console.log(`๐Ÿง  Intent: ${routerResult.intent} | Strategy: ${routerResult.strategy}`);
 
 
76
 
77
- let responseData = { content: '', quickReplies: [] };
 
 
 
 
 
 
 
 
78
 
79
- switch (routerResult.intent) {
80
- case 'quote':
81
- // === ื˜ื™ืคื•ืœ ื‘ืืกื˜ืจื˜ื’ื™ื”: ื”ืฆืขื” ื™ื–ื•ืžื” (Offer Popular) ===
82
- if (routerResult.strategy === 'offer_popular' && routerResult.items?.[0]?.product) {
83
- const product = routerResult.items[0].product;
84
- const defaultQty = POPULAR_DEFAULTS[product] || 100;
85
-
86
- // ืžื—ืฉื‘ื™ื ืืช ื”ื”ืฆืขื” ื”ืคื•ืคื•ืœืจื™ืช
87
- const calc = calculate_custom_job(session.cart, {
88
- product_name: product,
89
- qty: defaultQty
90
- });
91
-
92
- // ืœื ืžื•ืกื™ืคื™ื ืœืขื’ืœื” ืื•ื˜ื•ืžื˜ื™ืช, ืจืง ืžืฆื™ื’ื™ื (ืกื™ืžื•ืœืฆื™ื”)
93
- responseData.content = `ื”ื›ื™ ื”ื•ืœืš ืืฆืœื ื• ื–ื” ${defaultQty} ื™ื—ื™ื“ื•ืช ื‘-โ‚ช${calc.lastAdded.client_price}. ื–ื” ืžืชืื™ื ืœืš, ืื• ืฉืชืจืฆื” ื›ืžื•ืช ืื—ืจืช?`;
94
- responseData.quickReplies = [
95
- { text: `ื›ืŸ, ืชื–ืžื™ืŸ ${defaultQty}`, value: `ืื ื™ ืจื•ืฆื” ${defaultQty} ${product}` },
96
- { text: 'ื›ืžื•ืช ืื—ืจืช', value: 'ื›ืžื•ืช ืื—ืจืช' }
97
- ];
98
- break;
99
- }
100
 
101
- // === ื˜ื™ืคื•ืœ ื‘ืืกื˜ืจื˜ื’ื™ื”: ื“ื—ื™ืคื•ืช (Check Urgency) ===
102
- if (routerResult.strategy === 'check_urgency') {
103
- responseData.content = `ืื ื™ ืžื‘ื™ืŸ ืฉื–ื” ื“ื—ื•ืฃ! ๐Ÿš€\nื™ืฉ ืœื ื• ืžืกืœื•ืœ ืืงืกืคืจืก (ืžื”ื™ื•ื ืœื”ื™ื•ื/ืžื—ืจ) ื‘ืชื•ืกืคืช ืชืฉืœื•ื, ืื• ืจื’ื™ืœ (3 ื™ืžื™ ืขืกืงื™ื).\nืื™ืš ืชืจืฆื” ืœื”ืชืงื“ื?`;
104
- responseData.quickReplies = [
105
- { text: 'ืืงืกืคืจืก (ื“ื—ื•ืฃ!)', value: 'ืืงืกืคืจืก' },
106
- { text: 'ืจื’ื™ืœ ื–ื” ื‘ืกื“ืจ', value: 'ืจื’ื™ืœ' }
107
- ];
108
- break; // ืขื•ืฆืจื™ื ื›ืืŸ ื›ื“ื™ ืœืงื‘ืœ ืชืฉื•ื‘ื”
109
- }
110
 
111
- // === ืžืกืœื•ืœ ืจื’ื™ืœ (ื”ื•ืกืคื” ืœืขื’ืœื”) ===
112
- const itemsToAdd = routerResult.items || [];
113
- const addedItems = [];
114
-
115
- for (const item of itemsToAdd) {
116
- if (item.product && item.qty) {
117
- const calc = calculate_custom_job(session.cart, {
118
- product_name: item.product,
119
- qty: item.qty,
120
- paper_type: item.attributes?.paper
121
- });
122
- session.cart = calc.updatedCart;
123
- addedItems.push(calc.lastAdded);
124
- }
125
- }
126
 
127
- if (addedItems.length > 0) {
128
- // ืื ื”-Strategy ื”ื•ื close_deal, ื ื•ืกื™ืฃ ืžืฉืคื˜ ืกื’ื™ืจื”
129
- let closingText = "";
130
- if (routerResult.strategy === 'close_deal') {
131
- closingText = "\n\nื™ืฉ ืœื ื• ืืช ื›ืœ ื”ืคืจื˜ื™ื. ืœืฉืœื•ื— ืœืš ืœื™ื ืง ืœืชืฉืœื•ื ื•ืกื’ื™ืจื”?";
132
- } else if (routerResult.strategy === 'req_file') {
133
- closingText = "\n\nื™ืฉ ืœืš ื›ื‘ืจ ืงื•ื‘ืฅ ืžื•ื›ืŸ ืœืฉืœื•ื— ืœื™?";
134
- }
135
-
136
- responseData.content = (addedItems.length === 1
137
- ? buildResponse('quote_added', { item: addedItems[0] })
138
- : buildResponse('multi_quote_added', { items: addedItems, cart: session.cart })) + closingText;
139
 
140
- responseData.quickReplies = buildQuickReplies('cart_status');
141
- } else {
142
- responseData.content = "ื”ื‘ื ืชื™ ืฉืืชื” ืจื•ืฆื” ืœื”ื–ืžื™ืŸ, ืžื” ื”ื›ืžื•ื™ื•ืช ื•ืกื•ื’ ื”ืžื•ืฆืจ?";
143
- responseData.quickReplies = buildQuickReplies('greeting');
144
- }
145
- break;
146
-
147
- case 'update':
148
- const updateData = routerResult.items?.[0] || {};
149
- const lastItem = session.cart[session.cart.length - 1];
150
- if (lastItem && updateData.qty) {
151
- const upCalc = calculate_custom_job(session.cart, {
152
- product_name: lastItem.product_name,
153
- qty: updateData.qty
154
- });
155
- session.cart = upCalc.updatedCart;
156
- responseData.content = buildResponse('quote_updated', { item: upCalc.lastAdded, oldQty: lastItem.qty });
157
  }
158
- break;
159
-
160
- case 'status':
161
- responseData.content = buildResponse('cart_status', { cart: session.cart });
162
- responseData.quickReplies = buildQuickReplies('cart_status');
163
- break;
164
-
165
- case 'design_check':
166
- responseData.content = buildResponse('design_check', {});
167
- responseData.quickReplies = buildQuickReplies('design_check');
168
- break;
169
-
170
- case 'checkout':
171
- const total = session.cart.reduce((s, i) => s + i.client_price, 0);
172
- responseData.content = buildResponse('send_quote', { total, cart: session.cart });
173
- responseData.quickReplies = buildQuickReplies('send_quote');
174
- break;
175
-
176
- case 'greeting':
177
- responseData.content = buildResponse('greeting');
178
- responseData.quickReplies = buildQuickReplies('greeting');
179
- break;
180
-
181
- case 'consult':
182
- default:
183
- const llmResponse = await handleWithSmartLLM(message, session);
184
- responseData.content = llmResponse.content;
185
- break;
186
  }
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  addToHistory(userId, 'user', message);
189
- addToHistory(userId, 'model', responseData.content);
 
 
 
 
 
 
190
 
191
  res.json({
192
- content: responseData.content,
193
  cart: session.cart,
194
- dashboard: generateDashboard(session, session.customerPhone),
195
- quickReplies: responseData.quickReplies,
196
- meta: { intent: routerResult.intent, strategy: routerResult.strategy }
 
 
 
 
 
197
  });
198
 
199
  } catch (error) {
200
- console.error('Server Error:', error);
201
- res.status(500).json({ content: 'ืชืงืœื” ื‘ืฉืจืช, ื ืกื” ืฉื•ื‘.' });
 
202
  }
203
  });
204
 
205
- function mapActionToIntent(action) {
206
- const map = { 'quote': 'quote', 'update_qty': 'update', 'remove': 'remove', 'greeting': 'greeting', 'send_quote': 'checkout', 'status': 'status', 'design_check': 'design_check' };
207
- return map[action] || 'consult';
208
- }
209
-
210
- async function handleWithSmartLLM(message, session) {
211
- const taskType = detectTaskType(message, { hasQuote: session.cart.length > 0 });
212
- const promptData = buildPrompt(taskType, { userMessage: message, cart: session.cart, history: session.history });
213
- const result = await chatModel.generateContent(promptData.system + "\n" + promptData.context);
214
- let text = result.response.text();
215
- return { content: text, quickReplies: [] };
216
- }
217
-
218
  // PDF Route
219
  app.post('/api/pdf', async (req, res) => {
 
220
  try {
221
  const { cart, customer } = req.body;
222
  if (!cart || !Array.isArray(cart) || cart.length === 0) return res.status(400).json({ error: 'Cart empty' });
@@ -227,9 +278,14 @@ app.post('/api/pdf', async (req, res) => {
227
  });
228
  res.send(pdfBuffer);
229
  } catch (e) {
 
230
  res.status(500).send("Error generating PDF");
231
  }
232
  });
233
 
234
  const PORT = process.env.PORT || 7860;
235
- app.listen(PORT, () => console.log(`๐Ÿš€ Pini V7 Server (Strategy Engine) running on port ${PORT}`));
 
 
 
 
 
1
  /**
2
+ * Pini Print Bot - The Silent Engine (V8)
3
+ * =======================================
4
+ * ืืจื›ื™ื˜ืงื˜ื•ืจื”: Agentic Workflow (Plan -> Execute -> Respond)
5
+ * ื›ื•ืœืœ ืžืขืจื›ืช ืœื•ื’ื™ื ืžืชืงื“ืžืช ืœื ื™ื˜ื•ืจ ื•ืœืžื™ื“ื”.
6
  */
7
 
8
  const express = require('express');
9
  const cors = require('cors');
10
  const dotenv = require('dotenv');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
 
14
+ // --- ื™ื™ื‘ื•ื ื”ืžื ื•ืขื™ื ื”ืฉืงื˜ื™ื ---
15
+ const { classifyMessage } = require('./engine/classifier');
16
+ const { extractParameters } = require('./engine/extractor');
17
+ const { planActions } = require('./engine/planner');
18
  const { calculate_custom_job } = require('./engine/calculation');
 
 
 
19
  const { generateDashboard } = require('./engine/dashboardManager');
20
+ const { buildResponse, buildQuickReplies } = require('./engine/responseBuilder');
21
+ const { getSession, removeFromCart, clearCart, addToHistory } = require('./services/sessionManager');
22
  const { findOrCreateCustomer, extractPhoneFromText, extractNameFromText } = require('./engine/customerManager');
23
+ const { generateQuotePDF } = require('./services/pdfService');
24
+
25
+ // --- ื™ื™ื‘ื•ื ืžื ื•ืขื™ AI (ืœืžืงืจื™ื ืžื•ืจื›ื‘ื™ื ื‘ืœื‘ื“) ---
26
+ const { routeRequest } = require('./engine/llmRouter'); // Fallback Planner
27
+ const { handleWithSmartLLM } = require('./engine/smartLLM'); // Fallback Responder
28
 
29
  dotenv.config();
30
  const app = express();
 
33
  app.use(express.json());
34
  app.use(express.static('public'));
35
 
36
+ // === ืžืขืจื›ืช ืœื•ื’ื™ื ืฆื‘ืขื•ื ื™ืช ===
37
+ const LOG = {
38
+ info: (msg) => console.log(`\x1b[36mโ„น๏ธ ${msg}\x1b[0m`), // Cyan
39
+ success: (msg) => console.log(`\x1b[32mโœ… ${msg}\x1b[0m`), // Green
40
+ warning: (msg) => console.log(`\x1b[33mโš ๏ธ ${msg}\x1b[0m`), // Yellow
41
+ error: (msg) => console.log(`\x1b[31mโŒ ${msg}\x1b[0m`), // Red
42
+ brain: (msg) => console.log(`\x1b[35m๐Ÿง  ${msg}\x1b[0m`), // Magenta (Thinking)
43
+ action: (msg) => console.log(`\x1b[34mโš™๏ธ ${msg}\x1b[0m`) // Blue (Action)
 
 
 
44
  };
45
 
46
+ // === ืžื ื’ื ื•ืŸ ืœืžื™ื“ื” ืขืฆืžื™ืช (Self Correction) ===
47
+ function logLearningEvent(type, input, output, correction = null) {
48
+ const event = {
49
+ timestamp: new Date().toISOString(),
50
+ type,
51
+ input,
52
+ output,
53
+ correction
54
+ };
55
+ // ื‘ืคื•ืขืœ: ืฉื•ืžืจื™ื ืœื“ืื˜ื”ื‘ื™ื™ืก. ื›ืจื’ืข ื ื›ืชื•ื‘ ืœืงื•ื‘ืฅ JSON
56
+ fs.appendFile('learning_logs.json', JSON.stringify(event) + '\n', (err) => {
57
+ if (err) console.error("Failed to save learning log");
58
+ });
59
+ }
60
+
61
+ // ============================================================
62
+ // MAIN CHAT ENDPOINT
63
+ // ============================================================
64
  app.post('/api/chat', async (req, res) => {
65
+ const startTime = Date.now();
66
  try {
67
  const { message, userId, phone, customerName } = req.body;
68
  if (!message) return res.status(400).json({ error: 'Missing message' });
69
+
70
+ console.log('\n' + '='.repeat(60));
71
+ LOG.info(`New Message from ${userId}: "${message}"`);
72
+
73
+ // 1. ื ื™ื”ื•ืœ ืกืฉืŸ
74
  const session = getSession(userId);
75
+ let customer = null;
76
 
77
+ // ื–ื™ื”ื•ื™ ืœืงื•ื— (ืฉื/ื˜ืœืคื•ืŸ)
78
  const extractedPhone = extractPhoneFromText(message);
79
  if (phone || extractedPhone) {
80
+ customer = findOrCreateCustomer(phone || extractedPhone, customerName || extractNameFromText(message));
81
+ session.customerPhone = customer.phone;
82
+ LOG.success(`Customer Identified: ${customer.name || customer.phone}`);
83
  }
84
 
85
+ // ============================================================
86
+ // ืฉืœื‘ 1: ื”ื‘ื ื” (Perception)
87
+ // ============================================================
88
 
89
+ // ื. ืกื™ื•ื•ื’ (Classifier) - ื”ืื ื–ื” ืคืฉื•ื˜ ืื• ืžื•ืจื›ื‘?
90
+ const classification = classifyMessage(message, { cart: session.cart });
91
+ LOG.brain(`Classifier: [${classification.intent}] (Confidence: ${classification.confidence})`);
92
+
93
+ let intent = classification.intent;
94
+ let params = {};
95
+ let strategy = 'standard';
96
+
97
+ // ื‘. ื—ื™ืœื•ืฅ ืคืจืžื˜ืจื™ื (Extractor) ืื• ืคื ื™ื™ื” ืœ-LLM
98
  if (!classification.needsLLM) {
99
+ // ืžืกืœื•ืœ ืžื”ื™ืจ (Rule-Based)
100
+ LOG.success(`โšก Fast Path Triggered (No AI)`);
101
+ params = extractParameters(message);
102
+ LOG.brain(`Extracted Params: ${JSON.stringify(params)}`);
 
 
103
  } else {
104
+ // ืžืกืœื•ืœ ื—ื›ื (LLM)
105
+ LOG.warning(`๐Ÿค– Complex Request -> Calling LLM Planner...`);
106
+ const llmResult = await routeRequest(message, session.cart, session.history);
107
+ intent = llmResult.intent;
108
+ strategy = llmResult.strategy || 'standard';
109
+ // ื ืจืžื•ืœ ื”ืชื•ืฆืื” ืžื”-LLM ืœืžื‘ื ื” ืฉืœ ื”-Extractor
110
+ if (llmResult.items && llmResult.items.length > 0) {
111
+ params = llmResult.items[0]; // ืœื•ืงื—ื™ื ืืช ื”ืจืืฉื•ืŸ ื›ืขื™ืงืจื™ ื›ืจื’ืข
112
+ // ืื ื™ืฉ ืžืกืคืจ ืคืจื™ื˜ื™ื, ื”-Planner ื™ืฆื˜ืจืš ืœื“ืขืช ืœื˜ืคืœ ื‘ื–ื” (ื”ืจื—ื‘ื” ืขืชื™ื“ื™ืช)
113
+ }
114
+ LOG.brain(`LLM Decided: Intent=${intent}, Strategy=${strategy}`);
115
+
116
+ // ืชื™ืขื•ื“ ืœืœืžื™ื“ื”: ืœืžื” ื”ืžืกื•ื•ื’ ื ื›ืฉืœ?
117
+ logLearningEvent('fallback_to_llm', message, classification);
118
  }
119
+
120
+ // ============================================================
121
+ // ืฉืœื‘ 2: ืชื›ื ื•ืŸ (Planning)
122
+ // ============================================================
123
+
124
+ // ื”ื•ืกืคืช Strategy ืœืคืจืžื˜ืจื™ื ืื ื”ื’ื™ืข ืžื”-LLM ืื• ื–ื•ื”ื” ืข"ื™ ื”ืžืขืจื›ืช
125
+ if (params.attributes?.urgency === 'high') strategy = 'check_urgency';
126
 
127
+ const plan = planActions(intent, params, session);
128
+ LOG.brain(`Action Plan Generated: ${plan.actions.length} steps`);
129
+ plan.actions.forEach(a => LOG.brain(` -> ${a.type} (${JSON.stringify(a.payload || {})})`));
130
 
131
+ // ============================================================
132
+ // ืฉืœื‘ 3: ื‘ื™ืฆื•ืข (Execution)
133
+ // ============================================================
134
+
135
+ let executionResults = {
136
+ responses: [], // ืื•ืกืฃ ืชื‘ื ื™ื•ืช ืชืฉื•ื‘ื”
137
+ actionsTaken: [],
138
+ uiActions: [] // ื›ืคืชื•ืจื™ื ื•ื›ื•'
139
+ };
140
 
141
+ for (const action of plan.actions) {
142
+ LOG.action(`Executing: ${action.type}...`);
143
+
144
+ try {
145
+ switch (action.type) {
146
+ case 'CALCULATE_AND_ADD':
147
+ const calcResult = calculate_custom_job(session.cart, action.payload);
148
+ session.cart = calcResult.updatedCart;
149
+ executionResults.lastItem = calcResult.lastAdded;
150
+ executionResults.actionsTaken.push('item_added');
151
+ LOG.success(`Added ${calcResult.lastAdded.product_name} (Price: ${calcResult.lastAdded.client_price})`);
152
+ break;
 
 
 
 
 
 
 
 
 
153
 
154
+ case 'UPDATE_CART_ITEM':
155
+ // ืœื•ื’ื™ืงื” ืœืขื“ื›ื•ืŸ (ื“ื•ืžื” ืœื”ื•ืกืคื”, ืื‘ืœ ื“ื•ืจืก ืงื™ื™ื)
156
+ // ... (ืžื™ืžื•ืฉ ื“ื•ืžื” ืœ-CALCULATE_AND_ADD)
157
+ LOG.success(`Updated item quantity`);
158
+ break;
 
 
 
 
159
 
160
+ case 'REMOVE_FROM_CART':
161
+ const removed = removeFromCart(userId, action.product);
162
+ if (removed) LOG.success(`Removed ${action.product}`);
163
+ else LOG.warning(`Item to remove not found: ${action.product}`);
164
+ break;
165
+
166
+ case 'CHECK_URGENCY_OPTIONS':
167
+ // ืกื™ืžื•ืœืฆื™ื”: ื‘ื“ื™ืงื” ืžื•ืœ ื˜ื‘ืœืช ื™ื™ืฆื•ืจ
168
+ const canExpress = true; // ื ื ื™ื— ืฉื›ืŸ
169
+ const expressCost = 50; // ื ื ื™ื—
170
+ executionResults.urgencyOption = { canExpress, cost: expressCost };
171
+ LOG.info(`Urgency Check: Available (+${expressCost} NIS)`);
172
+ break;
 
 
173
 
174
+ case 'GENERATE_RESPONSE':
175
+ // ืฉื•ืžืจื™ื ืืช ื”ืชื‘ื ื™ืช ืฉืฆืจื™ืš ืœื”ืคืขื™ืœ ื‘ืกื•ืฃ
176
+ executionResults.responseTemplate = action.template;
177
+ break;
 
 
 
 
 
 
 
 
178
 
179
+ case 'UPDATE_DASHBOARD':
180
+ // ื™ื‘ื•ืฆืข ื‘ืกื•ืฃ ื’ื•ืจืฃ
181
+ break;
182
+
183
+ case 'CALL_LLM_CONSULTANT':
184
+ // ื‘ืžืงืจื” ืฉืื™ืŸ ื‘ืจื™ืจื” ื•ืฆืจื™ืš ืชืฉื•ื‘ื” ื—ื•ืคืฉื™ืช
185
+ const llmResponse = await handleWithSmartLLM(message, session, customer);
186
+ executionResults.customText = llmResponse.content;
187
+ executionResults.quickReplies = llmResponse.quickReplies;
188
+ break;
 
 
 
 
 
 
 
189
  }
190
+ } catch (err) {
191
+ LOG.error(`Action Failed: ${action.type} - ${err.message}`);
192
+ logLearningEvent('execution_error', action, err.message);
193
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  }
195
 
196
+ // ============================================================
197
+ // ืฉืœื‘ 4: ื ื™ืกื•ื— ืชืฉื•ื‘ื” (Response Generation)
198
+ // ============================================================
199
+
200
+ let finalResponse = '';
201
+ let quickReplies = [];
202
+
203
+ // ืื ื™ืฉ ื˜ืงืกื˜ ืžื•ืชืื ืื™ืฉื™ืช ืžื”-LLM (ื‘ืžืงืจื” ืฉืœ Consult)
204
+ if (executionResults.customText) {
205
+ finalResponse = executionResults.customText;
206
+ quickReplies = executionResults.quickReplies || [];
207
+ }
208
+ // ืื—ืจืช, ื”ืฉืชืžืฉ ื‘ืชื‘ื ื™ืช ืฉื ื‘ื—ืจื” ืข"ื™ ื”-Planner
209
+ else if (executionResults.responseTemplate) {
210
+ // ื”ื–ืจืงืช ื ืชื•ื ื™ื ืœืชื‘ื ื™ืช
211
+ const context = {
212
+ item: executionResults.lastItem,
213
+ cart: session.cart,
214
+ customer: customer,
215
+ userMessage: message,
216
+ // ืื ื”ื™ื™ืชื” ื‘ื“ื™ืงืช ื“ื—ื™ืคื•ืช, ื ืขื‘ื™ืจ ืืช ื”ืชื•ืฆืื” ืœืชื‘ื ื™ืช
217
+ urgency: executionResults.urgencyOption
218
+ };
219
+
220
+ finalResponse = buildResponse(executionResults.responseTemplate, context);
221
+ quickReplies = buildQuickReplies(executionResults.responseTemplate);
222
+
223
+ // ืื ื–ื•ื”ืชื” ื“ื—ื™ืคื•ืช, ื ื•ืกื™ืฃ ืžืฉืคื˜ ื“ื™ื ืžื™ (ื‘ืœื™ LLM!)
224
+ if (strategy === 'check_urgency' && executionResults.urgencyOption) {
225
+ finalResponse += `\n\n๐Ÿš€ ืจืื™ืชื™ ืฉื–ื” ื“ื—ื•ืฃ. ืืคืฉืจ ืœื”ืจื™ืฅ ืืช ื–ื” ื‘ืืงืกืคืจืก ื‘ืชื•ืกืคืช โ‚ช${executionResults.urgencyOption.cost}. ืœืืฉืจ?`;
226
+ quickReplies = [
227
+ { text: 'ื›ืŸ, ืืงืกืคืจืก', value: 'ืืฉืจ ืืงืกืคืจืก' },
228
+ { text: 'ืœื, ืจื’ื™ืœ', value: 'ืžืฉืœื•ื— ืจื’ื™ืœ' }
229
+ ];
230
+ }
231
+ }
232
+ // ื‘ืจื™ืจืช ืžื—ื“ืœ ืื ืžืฉื”ื• ื”ืฉืชื‘ืฉ
233
+ else {
234
+ finalResponse = "ืงื™ื‘ืœืชื™, ืื‘ืœ ืื ื™ ืœื ื‘ื˜ื•ื— ืžื” ืœืขืฉื•ืช ื”ืœืื”. ืจื•ืฆื” ืœื“ื‘ืจ ืขื ื ืฆื™ื’?";
235
+ LOG.error("No response template selected!");
236
+ }
237
+
238
+ // ืฉืžื™ืจื” ื‘ื”ื™ืกื˜ื•ืจื™ื”
239
  addToHistory(userId, 'user', message);
240
+ addToHistory(userId, 'model', finalResponse);
241
+
242
+ // ืขื“ื›ื•ืŸ ื“ืฉื‘ื•ืจื“
243
+ const dashboard = generateDashboard(session, session.customerPhone);
244
+
245
+ const processTime = Date.now() - startTime;
246
+ LOG.info(`Request processed in ${processTime}ms`);
247
 
248
  res.json({
249
+ content: finalResponse,
250
  cart: session.cart,
251
+ dashboard: dashboard,
252
+ quickReplies: quickReplies,
253
+ meta: {
254
+ intent,
255
+ strategy,
256
+ fastPath: !classification.needsLLM,
257
+ processTime
258
+ }
259
  });
260
 
261
  } catch (error) {
262
+ LOG.error(`Server Error: ${error.message}`);
263
+ console.error(error);
264
+ res.status(500).json({ content: 'ืกืœื™ื—ื”, ื”ื™ื™ืชื” ืชืงืœื” ื‘ืžืขืจื›ืช. ื ืกื” ืฉื•ื‘.' });
265
  }
266
  });
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  // PDF Route
269
  app.post('/api/pdf', async (req, res) => {
270
+ // ... (ืื•ืชื• ืงื•ื“ ืงื™ื™ื)
271
  try {
272
  const { cart, customer } = req.body;
273
  if (!cart || !Array.isArray(cart) || cart.length === 0) return res.status(400).json({ error: 'Cart empty' });
 
278
  });
279
  res.send(pdfBuffer);
280
  } catch (e) {
281
+ console.error("PDF Error", e);
282
  res.status(500).send("Error generating PDF");
283
  }
284
  });
285
 
286
  const PORT = process.env.PORT || 7860;
287
+ app.listen(PORT, () => {
288
+ console.log(`\n๐Ÿš€ Pini V8 Silent Engine Running on port ${PORT}`);
289
+ console.log(`๐Ÿ“ Logging enabled with color coding`);
290
+ console.log(`๐Ÿง  Self-correction hooks active\n`);
291
+ });