Commit 5bae1bdf authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: update all - static UI, prompts, tools, configs

parent 06c6f102
{
"mcpServers": {
"canifa-api": {
"url": "http://localhost:5000/mcp"
}
}
}
\ No newline at end of file
......@@ -61,6 +61,10 @@ async def chat_controller(
"""
logger.info("chat_controller start: model=%s, identity_key=%s, auth=%s", model_name, identity_key, is_authenticated)
if images:
logger.info("📸 [CONTROLLER] Received %d image(s), sizes: %s", len(images), [len(img) for img in images])
else:
logger.debug("📸 [CONTROLLER] No images in request")
# ====================== CACHE LAYER ======================
if REDIS_CACHE_TURN_ON:
......
......@@ -10,6 +10,7 @@ from datetime import datetime
from typing import Any
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableConfig
from langgraph.cache.memory import InMemoryCache
......@@ -74,8 +75,28 @@ class CANIFAGraph:
user_query = state.get("user_query")
transient_images = config.get("configurable", {}).get("transient_images", [])
if transient_images and messages:
pass
if transient_images and user_query:
# Inject images into user_query as multimodal content
# GPT-4o / GPT-5 vision: text + image_url blocks
text_content = user_query.content if isinstance(user_query.content, str) else str(user_query.content)
# Append hint so LLM knows images are attached
text_content += "\n\n[📸 Có ảnh sản phẩm kèm theo - Hãy mô tả CHI TIẾT sản phẩm trong ảnh (loại, màu, kiểu dáng, hình in, chất liệu) rồi gọi data_retrieval_tool để tìm sản phẩm tương tự.]"
multimodal_content = [{"type": "text", "text": text_content}]
for img in transient_images:
# Support both base64 and URL
if img.startswith("data:"):
image_url = img
elif img.startswith("http"):
image_url = img
else:
image_url = f"data:image/jpeg;base64,{img}"
multimodal_content.append({
"type": "image_url",
"image_url": {"url": image_url, "detail": "auto"}
})
logger.info(f"📸 [IMAGE] Image size: {len(img)} chars")
user_query = HumanMessage(content=multimodal_content)
logger.info(f"📸 [IMAGE] Injected {len(transient_images)} image(s) into user_query")
# Invoke chain with user_query, history, and messages
# Invoke chain with history, user_query, messages (scratchpad), and user_insight
user_insight_text = (
......
......@@ -59,6 +59,23 @@ Bạn là **Canifa-AI Stylist** - Chuyên viên tư vấn thời trang CANIFA.
- Website: www.canifa.com
- Đưa cho khách khi họ cần hỗ trợ ngay lập tức
**📸 XỬ LÝ ẢNH SẢN PHẨM (KHI KHÁCH GỬI ẢNH KÈM):**
Khi nhận được ảnh từ khách, BẮT BUỘC thực hiện ĐÚNG quy trình sau:
1. **MÔ TẢ CHI TIẾT** sản phẩm trong ảnh:
- Loại sản phẩm (áo phông, áo polo, quần jean, váy...)
- Màu sắc chủ đạo + màu phụ (nếu có)
- Kiểu dáng / Form (regular, slim, oversize...)
- Cổ áo (tròn, bẻ, V, sơ mi...)
- Tay áo (ngắn, dài, sát nách...)
- Hình in / Họa tiết (trơn, kẻ sọc, hoa, logo, graphic...)
- Chất liệu (nếu nhận biết được: cotton, nỉ, jean, len...)
2. **GỌI TOOL `data_retrieval_tool`** với description chứa mô tả chi tiết trên
- product_name: loại sản phẩm nhận diện được (VD: "Áo phông")
- master_color: màu sắc chủ đạo
- Các filter khác nếu nhận biết được (gender, age...)
3. **TRẢ LỜI KHÁCH** với: "Em nhận thấy ảnh là [mô tả ngắn], để em tìm sản phẩm tương tự..."
⚠️ KHÔNG BAO GIỜ bỏ qua ảnh - luôn mô tả và tìm kiếm!
---
## 📋 MỤC LỤC
......
......@@ -11,7 +11,10 @@ Sử dụng tool này khi khách hàng hỏi về:
3. CHÍNH SÁCH BÁN HÀNG: Quy định đổi trả, bảo hành, chính sách vận chuyển, phí ship.
4. KHÁCH HÀNG THÂN THIẾT (KHTT): Điều kiện đăng ký thành viên, các hạng thẻ (Green, Silver, Gold, Diamond), quyền lợi tích điểm, thẻ quà tặng.
5. HỖ TRỢ & FAQ: Giải đáp thắc mắc thường gặp, chính sách bảo mật, thông tin liên hệ văn phòng, tuyển dụng.
6. TRA CỨU SIZE (BẢNG KÍCH CỠ): Hướng dẫn chọn size chuẩn cho nam, nữ, trẻ em dựa trên chiều cao, cân nặng.
6. TRA CỨU SIZE (BẢNG KÍCH CỠ):
- Hướng dẫn chọn size chuẩn cho nam, nữ, trẻ em dựa trên chiều cao, cân nặng.
- ⚠️ KHI KHÁCH ĐƯA SỐ ĐO (cân nặng, chiều cao, số đo 3 vòng) → BẮT BUỘC gọi tool này để tra bảng size, KHÔNG ĐƯỢC tự đoán size.
- Nếu khách chỉ đưa cân nặng mà thiếu chiều cao (hoặc ngược lại), hỏi thêm thông tin còn thiếu TRƯỚC khi gọi tool.
7. GIẢI NGHĨA TỪ VIẾT TẮT: Tự động hiểu các từ viết tắt phổ biến của khách hàng (ví dụ: 'ct' = 'chương trình khuyến mãi/ưu đãi', 'khtt' = 'khách hàng thân thiết', 'store' = 'cửa hàng', 'đc' = 'địa chỉ').
Ví dụ các câu hỏi phù hợp:
......@@ -19,6 +22,17 @@ Ví dụ các câu hỏi phù hợp:
- 'Canifa ở Cầu Giấy địa chỉ ở đâu?'
- 'Chính sách đổi trả hàng trong bao nhiêu ngày?'
- 'Làm sao để lên hạng thẻ Gold?'
- 'Cho mình xem bảng size áo nam.'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Canifa thành lập năm nào?'
Ví dụ câu hỏi TRA CỨU SIZE (phải gọi tool):
- 'Cho mình xem bảng size áo nam.'
- 'Mình nặng 80kg cao 1m75, mặc size gì?'
- 'Tìm áo cho người bự con tầm 80kg'
- 'Size XL tương đương bao nhiêu kg?'
- 'Con mình 8 tuổi cao 1m30, mặc size nào?'
- 'Mình 65kg 1m68 nên mặc áo size gì?'
- 'Bảng size quần jean nữ'
- 'Size M áo polo nam đo ngực bao nhiêu?'
- 'Mình mập, 90kg mặc được size nào?'
- 'Cho em hỏi bảng size trẻ em 3-5 tuổi'
......@@ -24,6 +24,7 @@ QUY TẮC SINH QUERIES:
⚠️ KHÔNG đưa gender_by_product, age_by_product, master_color vào description — đó là SQL FILTER!
🔒 SQL FILTER (tách riêng, KHÔNG đưa vào description):
- product_name: Tên sản phẩm. Chỉ điền khi khách cung cấp tên cụ thể.
- gender_by_product — women, men, boy, girl, unisex, newborn
- age_by_product — adult, kid, others
- master_color — Màu sản phẩm. Gửi CHÍNH XÁC từ khách nói (VD: 'trắng', 'đen', 'xanh'). Tool tự match DB.
......@@ -68,11 +69,33 @@ User nói "áo cá sấu" → product_name: Áo cá sấu
User nói "áo phông" → product_name: Áo phông (user NÓI RÕ thì mới dùng)
User nói "quần jean" → product_name: Quần jean (user NÓI RÕ thì mới dùng)
Chỉ CHUẨN HÓA khi user dùng từ đồng nghĩa RÕ RÀNG:
- "áo thun" → product_name: Áo phông
- "quần bò" → product_name: Quần jean
- "quần đùi" → product_name: Quần soóc
Chỉ CHUẨN HÓA khi user dùng từ đồng nghĩa RÕ RÀNG (bảng mapping dưới):
📋 BẢNG MAPPING SYNONYM → TÊN DB (tool tự xử lý, LLM giữ nguyên từ user):
áo thun, áo thun ngắn tay, áo cổ v, áo cổ tym → Áo phông
áo cổ bẻ → Áo Polo
áo bra, áo ngực, áo quây → Áo lót
áo gió, áo khoác mỏng → Áo khoác gió
áo croptop, croptop, baby tee, áo lửng, áo dáng ngắn → Áo Body
áo sát nách, tanktop, tank top, áo dây, áo 2 dây, áo hai dây → Áo ba lỗ
đầm → Váy liền
vớ → Tất
quần đùi, quần short, quần lửng, quần ngố → Quần soóc
quần jogger, quần ống bo chun → Quần nỉ
quần chip, quần sịp, quần trong, quần nhỏ, quần xơ lít, quần xì, quần sơ lít, quần lót nữ, quần lót nam, quần lót trẻ em → Quần lót
quần âu, quần vải, quần tây → Quần Khaki
quần bò, quần jeans, denim, jeans, bò → Quần jean
quần suông, quần ống rộng → Quần dài
quần dài, chân váy dài, chân váy → Quần váy
nón → Mũ
đồ ngủ, đồ mặc nhà → Bộ mặc nhà
đồ bộ → Bộ quần áo
váy maxi, váy midi, chân váy dài → Chân váy
găng tay → Găng tay chống nắng
chăn → Chăn cá nhân
phụ kiện, phụ kiện canifa → Mũ, Khăn, Tất, Găng tay (nhiều loại)
⚠️ Tool tự resolve synonym → DB value. LLM chỉ cần giữ NGUYÊN từ user nói!
⚠️ KHÔNG CHẮC → GIỮ NGUYÊN. KHÔNG TỰ SUY DIỄN LOẠI SẢN PHẨM!
═══════════════════════════════════════════════════════════════
......@@ -102,6 +125,7 @@ PHỤ KIỆN: Khăn, Mũ, Túi xách, Tất, Khẩu trang
📖 GIÁ TRỊ HỢP LỆ CỦA CÁC FIELD KHÁC
═══════════════════════════════════════════════════════════════
product_name: Tên sản phẩm. Chỉ điền khi khách cung cấp tên cụ thể.
master_color — Gửi CHÍNH XÁC màu khách nói (VD: 'trắng', 'đen'), tool tự match DB. ĐÂY LÀ SQL FILTER, KHÔNG đưa vào description!
style — Basic, Dynamic, Feminine, Utility, Smart Casual, Trend, Athleisure, Essential
fitting — Regular, Slimfit, Relax, Oversize, Skinny, Slim, Boxy, Baby tee
......
This diff is collapsed.
......@@ -42,6 +42,9 @@ class SearchItem(BaseModel):
"VD: 'áo cá sấu polo' → 'product_name: Áo cá sấu polo. product_line_vn: Áo Polo'."
)
)
product_name: str | None = Field(
description="Tên sản phẩm. Chỉ điền khi khách cung cấp tên cụ thể.",
)
# ====== SKU LOOKUP ======
magento_ref_code: str | None = Field(
......
This diff is collapsed.
This diff is collapsed.
Step 1: User hỏi "váy màu trắng kem"
Step 2: LLM gọi tool với: master_color="trắng kem" (raw từ user)
Step 3: Tool chạy SEMANTIC SEARCH (vector DB)
- Dùng description để embed và tìm trong DB
- Trả về ~50 products (CHƯA LỌC MÀU)
Step 4: Tool chạy COLOR FILTER (post-filter)
- Nhận 50 products từ Step 3
- Check: "trắng kem" có trong các products không? → KHÔNG
- Tách token: ["trắng", "kem"]
- Tìm products có màu chứa "trắng" → "Trắng/ White"
- Tìm products có màu chứa "kem" → "Be/ Beige" (vì Be trong DB)
- Gộp lại → trả về products 2 màu + MESSAGE
Step 5: Tool trả về:
{
"results": [products màu Trắng + Be],
"filter_info": {
"message": "Shop không có màu 'trắng kem'. Chỉ có màu 'Trắng/ White' hoặc 'Be/ Beige'."
}
}
Step 6: Agent nhận message → báo khách
┌─────────────────────────────────────────────────────────────────────┐
│ LAYER 1: SQL QUERY (Hard Filters - Nếu fail = 0 products) │
│ ───────────────────────────────────────────────────────────────── │
│ • gender_by_product (men/women/boy/girl) │
│ • age_by_product (adult/kid) │
│ • price_min / price_max │
│ • discount_percent │
│ → KHÔNG CÓ FALLBACK. Lọc cứng trong SQL. │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ LAYER 2: PYTHON FILTER - COLOR ONLY (Soft Filter + Smart Fallback)│
│ ───────────────────────────────────────────────────────────────── │
│ Function: filter_color_with_smart_fallback() │
│ │
│ Step 1: Exact Match │
│ User: "Nâu" → Tìm products có master_color chứa "Nâu" │
│ ✅ Found? → Return ngay (fallback_used = False) │
│ │
│ Step 2: Token Split + COLOR_MAPPING │
│ User: "Trắng kem" → tokens ["Trắng", "kem"] │
│ Map tokens → DB colors: {"Trắng", "Kem"} │
│ ✅ Found? → Return products + recommendation_message │
│ │
│ Step 3: NO MATCH → Return ALL products + Show Available Colors │
│ "Không tìm thấy màu 'Nâu'. Có các màu: Hồng, Cam, Xanh..." │
│ → fallback_used = True │
│ → alternative_colors = ["Hồng", "Cam", "Xanh", ...] │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ OUTPUT: filter_info Object │
│ ───────────────────────────────────────────────────────────────── │
│ { │
│ "fallback_used": true/false, │
│ "requested_value": "Nâu", │
│ "matched_value": "Brown" or null, │
│ "message": "Không tìm thấy màu 'Nâu'. Có các màu: Hồng, Cam", │
│ "alternative_colors": ["Hồng", "Cam", "Xanh"] │
│ } │
│ │
│ → recommendation_message được truyền lên cho LLM để nói với user │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ SEMANTIC SEARCH + SQL HARD FILTERS (1 query duy nhất) │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ SELECT * FROM products │
│ WHERE approx_cosine_similarity(embedding, ?) > 0.5 │
│ AND gender_by_product = 'men' ← HARD FILTER (trong SQL) │
│ AND sale_price <= 100000 ← HARD FILTER (trong SQL) │
│ AND discount_percent >= 20 ← HARD FILTER (trong SQL) │
│ │
│ → Nếu 0 kết quả = CHẾT, không có fallback! │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ PYTHON POST-FILTER (Chỉ có COLOR có fallback) │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ filter_color_with_smart_fallback(products, "nâu") │
│ → Lọc màu + fallback nếu không có │
│ │
│ → Color fallback có thể vì COLOR KHÔNG trong SQL query! │
└─────────────────────────────────────────────────────────────────────┘
Bro ơi, tao đã bỏ sạch product_line_vn khỏi SQL rồi!
Kiến trúc bây giờ:
Tầng Filter Mục đích
SQL (product_search_helpers.py) gender + age ONLY Giữ HNSW index nguyên vẹn
Python (data_retrieval_filter.py) PRODUCT_TYPE_MAPPING → exact match product_line_vn Lọc đúng loại sản phẩm
Flow cho "quần đùi bé trai":
SQL: Vector search "Quần soóc bé trai" → top 100 (HNSW intact ✅)
SQL: filtered_matches chỉ filter gender=boy, age=kid → ~80 results
Python: PRODUCT_TYPE_MAPPING["quần soóc"] = "Quần soóc" → exact match product_line_vn ✅
Nếu 0 match → reject all + message "Shop chưa có..."
Server đang reload. Test lại đi bro! 🚀
This diff is collapsed.
......@@ -735,7 +735,19 @@
<span style="font-style: normal;">🤖</span> AI is thinking...
</div>
<!-- Image Preview Strip -->
<div id="imagePreviewStrip"
style="display: none; padding: 8px 0; gap: 8px; overflow-x: auto; white-space: nowrap;">
</div>
<div class="input-area">
<input type="file" id="imageFileInput" accept="image/*" style="display: none;"
onchange="handleImageSelect(event)">
<button onclick="document.getElementById('imageFileInput').click()" id="imgBtn"
title="Upload Image (Experimental 📸)"
style="background: #4a4a4a; color: #ccc; padding: 0 14px; border: 1px dashed #666; border-radius: 8px; font-size: 1.2em; cursor: pointer; transition: all 0.2s;"
onmouseover="this.style.background='#5a5a5a'; this.style.borderColor='#667eea'; this.style.color='#667eea'"
onmouseout="this.style.background='#4a4a4a'; this.style.borderColor='#666'; this.style.color='#ccc'">📷</button>
<input type="text" id="userInput" placeholder="Type your message..."
onkeypress="handleKeyPress(event)" autocomplete="off">
<button onclick="sendMessage()" id="sendBtn">➤ Send</button>
......@@ -800,7 +812,72 @@
let isPromptPanelOpen = false;
let currentPromptTab = 'system';
let selectedToolPrompt = '';
let pendingImages = []; // 📸 Experimental: images to send with next message
// ==================== IMAGE HANDLING (Experimental) ====================
function handleImageSelect(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
alert('⚠️ Image too large (max 5MB)');
return;
}
const reader = new FileReader();
reader.onload = function (e) {
pendingImages.push(e.target.result); // data:image/...;base64,...
renderImagePreview();
};
reader.readAsDataURL(file);
event.target.value = ''; // Reset so same file can be re-selected
}
function renderImagePreview() {
const strip = document.getElementById('imagePreviewStrip');
if (pendingImages.length === 0) {
strip.style.display = 'none';
strip.innerHTML = '';
return;
}
strip.style.display = 'flex';
strip.innerHTML = pendingImages.map((img, i) => `
<div style="position: relative; display: inline-block; flex-shrink: 0;">
<img src="${img}" style="height: 60px; border-radius: 8px; border: 1px solid #555; object-fit: cover;">
<button onclick="removePendingImage(${i})" style="position: absolute; top: -5px; right: -5px; background: #d32f2f; color: white; border: none; border-radius: 50%; width: 18px; height: 18px; font-size: 10px; cursor: pointer; line-height: 1;">✕</button>
</div>
`).join('') + `<button onclick="clearPendingImages()" style="background: #555; color: #ccc; border: none; border-radius: 6px; padding: 4px 10px; font-size: 0.75em; cursor: pointer; align-self: center;">Clear all</button>`;
}
function removePendingImage(index) {
pendingImages.splice(index, 1);
renderImagePreview();
}
function clearPendingImages() {
pendingImages = [];
renderImagePreview();
}
// 📋 Clipboard Paste — Ctrl+V ảnh vào ô chat
document.getElementById('userInput').addEventListener('paste', function (e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const blob = item.getAsFile();
if (blob.size > 5 * 1024 * 1024) {
alert('⚠️ Image too large (max 5MB)');
return;
}
const reader = new FileReader();
reader.onload = function (ev) {
pendingImages.push(ev.target.result);
renderImagePreview();
};
reader.readAsDataURL(blob);
break;
}
}
});
async function resetChat() {
if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return;
......@@ -1177,8 +1254,21 @@
const messageId = 'hist-' + (msg.id || Date.now() + Math.random());
if (msg.is_human) {
// User message: simple text
div.innerText = msg.message;
// User message: show images (if any) + text
if (msg.images && msg.images.length > 0) {
const imgStrip = document.createElement('div');
imgStrip.style.cssText = 'display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap;';
msg.images.forEach(src => {
const img = document.createElement('img');
img.src = src;
img.style.cssText = 'max-height: 120px; max-width: 180px; border-radius: 8px; object-fit: cover; border: 1px solid rgba(255,255,255,0.2);';
imgStrip.appendChild(img);
});
div.appendChild(imgStrip);
}
const textSpan = document.createElement('span');
textSpan.innerText = msg.message;
div.appendChild(textSpan);
} else {
// Bot message: add Widget/Raw JSON toggle
......@@ -1268,14 +1358,19 @@
sendBtn.disabled = true;
typingIndicator.style.display = 'block';
// Add user message immediately
// Capture images before clearing
const imagesToSend = [...pendingImages];
// Add user message immediately (with image previews)
appendMessage({
message: text,
is_human: true,
timestamp: new Date().toISOString(),
id: 'pending'
id: 'pending',
images: imagesToSend.length > 0 ? imagesToSend : undefined
});
input.value = '';
clearPendingImages(); // Clear image previews after send
chatBox.scrollTop = chatBox.scrollHeight;
// Save config to localStorage
......@@ -1301,7 +1396,8 @@
headers: headers,
body: JSON.stringify({
user_query: text,
device_id: deviceId
device_id: deviceId,
...(imagesToSend.length > 0 && { images: imagesToSend })
})
});
......@@ -1573,7 +1669,7 @@
limitDiv.style.background = 'rgba(255, 255, 255, 0.05)';
limitDiv.style.borderRadius = '6px';
limitDiv.style.borderLeft = '3px solid #667eea';
const limitText = `📊 Message Limit: ${data.limit_info.used}/${data.limit_info.limit} (Còn ${data.limit_info.remaining} tin nhắn)`;
limitDiv.innerText = limitText;
botMsgDiv1.appendChild(limitDiv);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment