Spaces:
Sleeping
Sleeping
Phase 2 Sprint 2.2: Vision Isolation - Multimodal AI & Frontend Attachment Pipeline
Browse files- config/prompts/wizardPrompt.txt +6 -1
- db/usage.json +1 -1
- engine/classifier.js +20 -2
- public/index.html +66 -6
- services/llmService.js +19 -9
- services/storageService.js +17 -1
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
|
|
|
|
| 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.
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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 |
};
|