dotandru commited on
Commit
628ec36
ยท
1 Parent(s): 90c5d59

Phase 2 Sprint 2.2: Vision Isolation - Multimodal AI & Frontend Attachment Pipeline

Browse files
config/prompts/wizardPrompt.txt CHANGED
@@ -18,7 +18,12 @@ AVAILABLE PRODUCTS: bc, flyer, booklet, rollup, sticker, poster, invitation, pla
18
  - Put a natural Hebrew response in `answer_text` explicitly recommending products from the available list.
19
  - Return relevant product keys in the array `recommended_products` (e.g., ["invitation", "place_card", "sticker"]).
20
 
21
- 4. **Output JSON format MUST be exactly**:
 
 
 
 
 
22
  {
23
  "intent": "quote" | "consult" | "chat" | "remove" | "reset" | "update",
24
  "product": "product_key" | null,
 
18
  - Put a natural Hebrew response in `answer_text` explicitly recommending products from the available list.
19
  - Return relevant product keys in the array `recommended_products` (e.g., ["invitation", "place_card", "sticker"]).
20
 
21
+ 5. **VISION ISOLATION**:
22
+ - When an image/file is uploaded, analyze ONLY the technical aspects (Resolution, DPI, Color Mode, Dimensions, Text elements).
23
+ - DO NOT decide if the file is "ready for print". That is the job of the Decision Kernel.
24
+ - Simply report the technical facts in `answer_text`.
25
+
26
+ 6. **Output JSON format MUST be exactly**:
27
  {
28
  "intent": "quote" | "consult" | "chat" | "remove" | "reset" | "update",
29
  "product": "product_key" | null,
db/usage.json CHANGED
@@ -1,4 +1,4 @@
1
  {
2
- "dailyCost": 5.1,
3
  "lastReset": "2026-02-25"
4
  }
 
1
  {
2
+ "dailyCost": 0,
3
  "lastReset": "2026-02-25"
4
  }
engine/classifier.js CHANGED
@@ -1,6 +1,7 @@
1
  /** engine/classifier.js V98.0 - Data Integrity Fix */
2
  const { validateLLMResult } = require('./validator');
3
  const { routeWithLLM } = require('../services/llmService');
 
4
 
5
  const KEYWORDS = {
6
  reset: ['reset', 'ื”ืชื—ืœ', 'ืื™ืคื•ืก', 'ืจื™ืกื˜', 'ืชืคืจื™ื˜'],
@@ -107,8 +108,25 @@ async function classify(text, session) {
107
 
108
  // 4. LLM Pipeline (ืœืžืœืœ ื—ื•ืคืฉื™ ื›ืžื• "500")
109
  try {
110
- const llmResult = await routeWithLLM(safeText, session);
111
- console.log(`\x1b[35m๐Ÿ” [X-RAY CLASSIFIER] Intent: ${llmResult.intent || 'chat'} (LLM Inference)\x1b[0m`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  console.log(`๐Ÿค– [LLM RAW RESULT]:`, JSON.stringify(llmResult));
113
  const validated = validateLLMResult(llmResult, safeText, session);
114
 
 
1
  /** engine/classifier.js V98.0 - Data Integrity Fix */
2
  const { validateLLMResult } = require('./validator');
3
  const { routeWithLLM } = require('../services/llmService');
4
+ const { downloadFile } = require('../services/storageService');
5
 
6
  const KEYWORDS = {
7
  reset: ['reset', 'ื”ืชื—ืœ', 'ืื™ืคื•ืก', 'ืจื™ืกื˜', 'ืชืคืจื™ื˜'],
 
108
 
109
  // 4. LLM Pipeline (ืœืžืœืœ ื—ื•ืคืฉื™ ื›ืžื• "500")
110
  try {
111
+ let imageBuffer = null;
112
+
113
+ // --- PHASE 2.2: Vision Attachment Handling ---
114
+ if (safeText.includes('[IMAGE_UPLOADED:')) {
115
+ const match = safeText.match(/\[IMAGE_UPLOADED:\s*([^\]]+)\]/);
116
+ if (match) {
117
+ const remotePath = match[1].trim();
118
+ console.log(`๐Ÿ“ธ [CLASSIFIER] Vision Request detected. Fetching: ${remotePath}`);
119
+ try {
120
+ imageBuffer = await downloadFile(remotePath);
121
+ } catch (e) {
122
+ console.error("Failed to fetch vision buffer:", e);
123
+ return { intent: 'chat', aiResponse: 'ื”ืงื•ื‘ืฅ ืขืœื” ืืš ืœื ื”ืฆืœื—ืชื™ ืœื ืชื— ืื•ืชื• ื˜ื›ื ื™ืช. ืชื•ื›ืœ ืœืฉืœื•ื— ืฉื•ื‘?' };
124
+ }
125
+ }
126
+ }
127
+
128
+ const llmResult = await routeWithLLM(safeText, session, imageBuffer);
129
+ console.log(`\x1b[35m๐Ÿ” [X-RAY CLASSIFIER] Intent: ${llmResult.intent || 'chat'} (${imageBuffer ? 'Vision' : 'LLM'} Inference)\x1b[0m`);
130
  console.log(`๐Ÿค– [LLM RAW RESULT]:`, JSON.stringify(llmResult));
131
  const validated = validateLLMResult(llmResult, safeText, session);
132
 
public/index.html CHANGED
@@ -417,7 +417,9 @@
417
 
418
  <div class="input-area">
419
  <div class="icon-btn" onclick="toggleMenu()" title="ืชืคืจื™ื˜ ืžื”ื™ืจ"><i class="fas fa-bars"></i></div>
420
- <div class="icon-btn"><i class="fas fa-paperclip"></i></div>
 
 
421
  <div class="input-wrapper">
422
  <input type="text" id="user-input" placeholder="ื”ืงืœื“ ื”ื•ื“ืขื”..." autocomplete="off">
423
  </div>
@@ -468,7 +470,8 @@
468
  const el = document.getElementById('connection-status');
469
  if (el) {
470
  el.innerText = text;
471
- el.style.color = text.includes('ืžืงืœื™ื“') ? 'var(--primary-color)' : '#667781';
 
472
  }
473
  }
474
 
@@ -568,11 +571,66 @@
568
  }
569
 
570
  // --- Core Functions ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  async function downloadPDF() {
572
  if (currentCart.length === 0) return alert('ื”ืขื’ืœื” ืจื™ืงื”');
573
  addMsg("ืžืคื™ืง ืžืกืžืš PDF... โณ", "bot");
574
  try {
575
- const res = await fetch(PDF_URL, {
576
  method: 'POST',
577
  headers: { 'Content-Type': 'application/json' },
578
  body: JSON.stringify({ userId: userId, cart: currentCart })
@@ -611,7 +669,7 @@
611
 
612
  // --- TIKUN (FIX): Updated sendMessage to accept displayLabel ---
613
  async function sendMessage(text = null, displayLabel = null) {
614
- if (isProcessing) return;
615
 
616
  const inputField = document.getElementById('user-input');
617
  const valueToSend = text || inputField.value.trim(); // ืžื” ื ืฉืœื— ืœืฉืจืช (ืงื•ื“)
@@ -619,8 +677,10 @@
619
 
620
  if (!valueToSend) return;
621
 
622
- // ืžืฆื™ื’ื™ื ืœืžืฉืชืžืฉ ืืช ืžื” ืฉื”ื•ื ืืžื•ืจ ืœืจืื•ืช
623
- addMsg(textToDisplay, 'user');
 
 
624
 
625
  inputField.value = '';
626
  removeQuickReplies();
 
417
 
418
  <div class="input-area">
419
  <div class="icon-btn" onclick="toggleMenu()" title="ืชืคืจื™ื˜ ืžื”ื™ืจ"><i class="fas fa-bars"></i></div>
420
+ <div class="icon-btn" onclick="document.getElementById('file-input').click()"><i
421
+ class="fas fa-paperclip"></i></div>
422
+ <input type="file" id="file-input" style="display:none" onchange="handleFileUpload(this)">
423
  <div class="input-wrapper">
424
  <input type="text" id="user-input" placeholder="ื”ืงืœื“ ื”ื•ื“ืขื”..." autocomplete="off">
425
  </div>
 
470
  const el = document.getElementById('connection-status');
471
  if (el) {
472
  el.innerText = text;
473
+ el.innerText = text;
474
+ el.style.color = text.includes('ืžืงืœื™ื“') || text.includes('ืžืขืœื”') ? 'var(--primary-color)' : '#667781';
475
  }
476
  }
477
 
 
571
  }
572
 
573
  // --- Core Functions ---
574
+ async function handleFileUpload(input) {
575
+ const file = input.files[0];
576
+ if (!file) return;
577
+
578
+ // 10MB client-side check
579
+ if (file.size > 10 * 1024 * 1024) {
580
+ alert("ืงื•ื‘ืฅ ื’ื“ื•ืœ ืžื“ื™ (ืžืงืกื™ืžื•ื 10MB)");
581
+ input.value = '';
582
+ return;
583
+ }
584
+
585
+ isProcessing = true;
586
+ updateStatus('ืžืขืœื” ืงื•ื‘ืฅ...');
587
+ addMsg(`ืžืขืœื” ืงื•ื‘ืฅ: ${file.name}...`, 'user');
588
+
589
+ try {
590
+ // 1. Get Signed URL
591
+ const signedRes = await fetch(`${PDF_URL}/signed-url`, {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/json' },
594
+ body: JSON.stringify({ userId: userId, fileName: file.name, fileSize: file.size })
595
+ });
596
+
597
+ if (!signedRes.ok) {
598
+ const error = await signedRes.json();
599
+ throw new Error(error.message || "ื ื›ืฉืœ ื‘ื‘ืงืฉืช ื”ื›ืชื•ื‘ืช ืœื”ืขืœืื”");
600
+ }
601
+
602
+ const { uploadUrl, remotePath } = await signedRes.json();
603
+
604
+ // 2. Direct upload to GCP
605
+ const uploadRes = await fetch(uploadUrl, {
606
+ method: 'PUT',
607
+ headers: { 'Content-Type': 'application/octet-stream' },
608
+ body: file
609
+ });
610
+
611
+ if (!uploadRes.ok) throw new Error("ื”ืขืœืืช ื”ืงื•ื‘ืฅ ืœืฉืจืช ื ื›ืฉืœื”");
612
+
613
+ addMsg("ื”ืงื•ื‘ืฅ ืขืœื” ื‘ื”ืฆืœื—ื”! ๐Ÿ–ผ๏ธ", "bot");
614
+ updateStatus('ืžื›ื™ืŸ ื ื™ืชื•ื—...');
615
+
616
+ // 3. Send system message to chat
617
+ await sendMessage(`[IMAGE_UPLOADED: ${remotePath}]`, "ืžื ืชื— ืงื•ื‘ืฅ...");
618
+
619
+ } catch (e) {
620
+ console.error(e);
621
+ addMsg(`ืฉื’ื™ืื” ื‘ื”ืขืœืื”: ${e.message}`, 'bot');
622
+ } finally {
623
+ input.value = '';
624
+ isProcessing = false;
625
+ updateStatus('ืžื—ื•ื‘ืจ');
626
+ }
627
+ }
628
+
629
  async function downloadPDF() {
630
  if (currentCart.length === 0) return alert('ื”ืขื’ืœื” ืจื™ืงื”');
631
  addMsg("ืžืคื™ืง ืžืกืžืš PDF... โณ", "bot");
632
  try {
633
+ const res = await fetch(`${PDF_URL}/quote`, {
634
  method: 'POST',
635
  headers: { 'Content-Type': 'application/json' },
636
  body: JSON.stringify({ userId: userId, cart: currentCart })
 
669
 
670
  // --- TIKUN (FIX): Updated sendMessage to accept displayLabel ---
671
  async function sendMessage(text = null, displayLabel = null) {
672
+ if (isProcessing && !text?.startsWith('[IMAGE_UPLOADED')) return;
673
 
674
  const inputField = document.getElementById('user-input');
675
  const valueToSend = text || inputField.value.trim(); // ืžื” ื ืฉืœื— ืœืฉืจืช (ืงื•ื“)
 
677
 
678
  if (!valueToSend) return;
679
 
680
+ // ืžืฆื™ื’ื™ื ืœืžืฉืชืžืฉ ืืช ืžื” ืฉื”ื•ื ืืžื•ืจ ืœืจืื•ืช (ืืœื ืื ื–ื• ื”ื•ื“ืขืช ืžืขืจื›ืช ืคื ื™ืžื™ืช ืฉืœ ื”ืขืœืื”)
681
+ if (!text?.startsWith('[IMAGE_UPLOADED')) {
682
+ addMsg(textToDisplay, 'user');
683
+ }
684
 
685
  inputField.value = '';
686
  removeQuickReplies();
services/llmService.js CHANGED
@@ -22,7 +22,7 @@ try {
22
  console.error("โŒ Failed loading wizardPrompt.txt");
23
  }
24
 
25
- async function routeWithLLM(message, session) {
26
  if (isBudgetExceeded()) {
27
  console.warn("๐Ÿ›‘ [BUDGET] Daily limit reached. Blocking LLM request.");
28
  return { intent: 'chat', answer_text: "ื”ืžืขืจื›ืช ื‘ืชื—ื–ื•ืงื” ืจื’ืขื™ืช (ื ื—ื–ื•ืจ ืœืคืขื™ืœื•ืช ืžืœืื” ืžื—ืจ ื‘ื‘ื•ืงืจ)", confidence: 0 };
@@ -39,19 +39,29 @@ async function routeWithLLM(message, session) {
39
  }
40
  });
41
 
42
- const finalPrompt = SYSTEM_PROMPT
43
- + `\n[CURRENT STATE]: Active Product: ${session.currentProduct || "None"}`
44
- + `\n[USER SAYS]: "${message}"\nJSON Output:`;
45
-
46
- console.log(`\n=================== AI REQUEST ===================`);
47
- console.log(`๐Ÿ“ [LLM] Payload length: ${finalPrompt.length} chars (~${Math.round(finalPrompt.length / 4)} tokens)`);
 
 
 
 
 
 
 
 
 
 
48
 
49
  // PHASE 1.3 Anti-Hallucination: Retry Strategy & Token-Level Parsing
50
  let attempts = 0;
51
  while (attempts < 2) {
52
  try {
53
  console.time(`โฑ๏ธ [LLM] Response Time (Attempt ${attempts + 1})`);
54
- const result = await model.generateContent(finalPrompt);
55
  console.timeEnd(`โฑ๏ธ [LLM] Response Time (Attempt ${attempts + 1})`);
56
 
57
  const usage = result.response.usageMetadata;
@@ -84,7 +94,7 @@ async function routeWithLLM(message, session) {
84
 
85
  // --- PHASE 1.3: Injecting Debug Metadata ---
86
  parsedObj._debug = {
87
- source: "LLM",
88
  tokens: tokenData,
89
  apiCost: apiCostStr
90
  };
 
22
  console.error("โŒ Failed loading wizardPrompt.txt");
23
  }
24
 
25
+ async function routeWithLLM(message, session, imageBuffer = null) {
26
  if (isBudgetExceeded()) {
27
  console.warn("๐Ÿ›‘ [BUDGET] Daily limit reached. Blocking LLM request.");
28
  return { intent: 'chat', answer_text: "ื”ืžืขืจื›ืช ื‘ืชื—ื–ื•ืงื” ืจื’ืขื™ืช (ื ื—ื–ื•ืจ ืœืคืขื™ืœื•ืช ืžืœืื” ืžื—ืจ ื‘ื‘ื•ืงืจ)", confidence: 0 };
 
39
  }
40
  });
41
 
42
+ // Multimodal Payload Preparation
43
+ const promptParts = [
44
+ { text: SYSTEM_PROMPT },
45
+ { text: `\n[CURRENT STATE]: Active Product: ${session.currentProduct || "None"}` },
46
+ { text: `\n[USER SAYS]: "${message}"\nJSON Output:` }
47
+ ];
48
+
49
+ if (imageBuffer) {
50
+ logAI("๐Ÿ“ท Image detected in payload. Activating Multimodal Vision.");
51
+ promptParts.push({
52
+ inlineData: {
53
+ data: imageBuffer.toString('base64'),
54
+ mimeType: "image/jpeg"
55
+ }
56
+ });
57
+ }
58
 
59
  // PHASE 1.3 Anti-Hallucination: Retry Strategy & Token-Level Parsing
60
  let attempts = 0;
61
  while (attempts < 2) {
62
  try {
63
  console.time(`โฑ๏ธ [LLM] Response Time (Attempt ${attempts + 1})`);
64
+ const result = await model.generateContent({ contents: [{ role: "user", parts: promptParts }] });
65
  console.timeEnd(`โฑ๏ธ [LLM] Response Time (Attempt ${attempts + 1})`);
66
 
67
  const usage = result.response.usageMetadata;
 
94
 
95
  // --- PHASE 1.3: Injecting Debug Metadata ---
96
  parsedObj._debug = {
97
+ source: imageBuffer ? "Vision" : "LLM",
98
  tokens: tokenData,
99
  apiCost: apiCostStr
100
  };
services/storageService.js CHANGED
@@ -41,6 +41,22 @@ async function generateSignedUploadUrl(sessionId, fileName) {
41
  }
42
  }
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  module.exports = {
45
- generateSignedUploadUrl
 
46
  };
 
41
  }
42
  }
43
 
44
+ /**
45
+ * Downloads a file from GCP bucket as a Buffer.
46
+ */
47
+ async function downloadFile(remotePath) {
48
+ try {
49
+ const file = storage.bucket(BUCKET_NAME).file(remotePath);
50
+ const [buffer] = await file.download();
51
+ console.log(`[STORAGE] Downloaded file buffer: ${remotePath}`);
52
+ return buffer;
53
+ } catch (error) {
54
+ console.error("Error downloading file from GCP:", error);
55
+ throw error;
56
+ }
57
+ }
58
+
59
  module.exports = {
60
+ generateSignedUploadUrl,
61
+ downloadFile
62
  };