Spaces:
Sleeping
Sleeping
56
Browse files- engine/classifier.js +73 -145
- engine/extractor.js +77 -0
- engine/planner.js +103 -0
- server.js +220 -164
engine/classifier.js
CHANGED
|
@@ -1,182 +1,110 @@
|
|
| 1 |
/**
|
| 2 |
-
* Pini
|
| 3 |
-
* =====================================
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
* 2. ืจืืืฉืืช ืืืืืืช ืงืืฉืืจ ("ืืื", "ืืื") ืืืจืืฉ ("ืฉืืืื", "ืคืืฆืื").
|
| 7 |
-
* 3. ืืืคืื ืืื ืืกืืืืก ืืืืจ.
|
| 8 |
*/
|
| 9 |
|
| 10 |
-
const
|
| 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 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
| 35 |
};
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
'
|
| 40 |
-
'
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
text = text.replace(word, num);
|
| 82 |
-
}
|
| 83 |
}
|
| 84 |
|
| 85 |
-
//
|
| 86 |
|
| 87 |
-
//
|
| 88 |
-
if (
|
| 89 |
-
return {
|
| 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 |
-
//
|
| 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 (
|
| 132 |
-
return {
|
| 133 |
}
|
| 134 |
|
| 135 |
-
//
|
| 136 |
-
if (
|
| 137 |
-
return {
|
| 138 |
}
|
| 139 |
|
| 140 |
-
//
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
}
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
(
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
}
|
| 156 |
|
| 157 |
-
//
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
return { action: 'quote_incomplete', data: { product }, needsLLM: false };
|
| 161 |
}
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
}
|
| 167 |
}
|
| 168 |
|
| 169 |
-
// ืืจืืจืช ืืืื
|
| 170 |
-
return {
|
| 171 |
}
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 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 -
|
| 3 |
-
* =======================================
|
| 4 |
-
*
|
|
|
|
| 5 |
*/
|
| 6 |
|
| 7 |
const express = require('express');
|
| 8 |
const cors = require('cors');
|
| 9 |
const dotenv = require('dotenv');
|
| 10 |
-
const
|
|
|
|
| 11 |
|
| 12 |
-
// ืืืืื ืื ืืขืื
|
| 13 |
-
const { classifyMessage } = require('./engine/classifier');
|
| 14 |
-
const {
|
|
|
|
| 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 {
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 32 |
-
const
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
-
//
|
| 60 |
-
|
| 61 |
-
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
if (!classification.needsLLM) {
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
items: [{ product: classification.data?.product, qty: classification.data?.qty }]
|
| 69 |
-
};
|
| 70 |
} else {
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 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 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
{ text: 'ืจืืื ืื ืืกืืจ', value: 'ืจืืื' }
|
| 107 |
-
];
|
| 108 |
-
break; // ืขืืฆืจืื ืืื ืืื ืืงืื ืชืฉืืื
|
| 109 |
-
}
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
}
|
| 125 |
-
}
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 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 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 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 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 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',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
res.json({
|
| 192 |
-
content:
|
| 193 |
cart: session.cart,
|
| 194 |
-
dashboard:
|
| 195 |
-
quickReplies:
|
| 196 |
-
meta: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
});
|
| 198 |
|
| 199 |
} catch (error) {
|
| 200 |
-
|
| 201 |
-
|
|
|
|
| 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, () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
});
|