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( ...@@ -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) 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 ====================== # ====================== CACHE LAYER ======================
if REDIS_CACHE_TURN_ON: if REDIS_CACHE_TURN_ON:
......
...@@ -10,6 +10,7 @@ from datetime import datetime ...@@ -10,6 +10,7 @@ from datetime import datetime
from typing import Any from typing import Any
from langchain_core.language_models import BaseChatModel from langchain_core.language_models import BaseChatModel
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.cache.memory import InMemoryCache from langgraph.cache.memory import InMemoryCache
...@@ -74,8 +75,28 @@ class CANIFAGraph: ...@@ -74,8 +75,28 @@ class CANIFAGraph:
user_query = state.get("user_query") user_query = state.get("user_query")
transient_images = config.get("configurable", {}).get("transient_images", []) transient_images = config.get("configurable", {}).get("transient_images", [])
if transient_images and messages: if transient_images and user_query:
pass # 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 user_query, history, and messages
# Invoke chain with history, user_query, messages (scratchpad), and user_insight # Invoke chain with history, user_query, messages (scratchpad), and user_insight
user_insight_text = ( user_insight_text = (
......
...@@ -59,6 +59,23 @@ Bạn là **Canifa-AI Stylist** - Chuyên viên tư vấn thời trang CANIFA. ...@@ -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 - Website: www.canifa.com
- Đưa cho khách khi họ cần hỗ trợ ngay lập tức - Đư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 ## 📋 MỤC LỤC
......
...@@ -11,7 +11,10 @@ Sử dụng tool này khi khách hàng hỏi về: ...@@ -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. 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. 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. 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ỉ'). 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: 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: ...@@ -19,6 +22,17 @@ Ví dụ các câu hỏi phù hợp:
- 'Canifa ở Cầu Giấy địa chỉ ở đâu?' - 'Canifa ở Cầu Giấy địa chỉ ở đâu?'
- 'Chính sách đổi trả hàng trong bao nhiêu ngày?' - 'Chính sách đổi trả hàng trong bao nhiêu ngày?'
- 'Làm sao để lên hạng thẻ Gold?' - '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?' - '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: ...@@ -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! ⚠️ 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): 🔒 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 - gender_by_product — women, men, boy, girl, unisex, newborn
- age_by_product — adult, kid, others - 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. - 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 ...@@ -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 "á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) 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: Chỉ CHUẨN HÓA khi user dùng từ đồng nghĩa RÕ RÀNG (bảng mapping dưới):
- "áo thun" → product_name: Áo phông
- "quần bò" → product_name: Quần jean 📋 BẢNG MAPPING SYNONYM → TÊN DB (tool tự xử lý, LLM giữ nguyên từ user):
- "quần đùi" → product_name: Quần soóc á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! ⚠️ 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 ...@@ -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 📖 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! 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 style — Basic, Dynamic, Feminine, Utility, Smart Casual, Trend, Athleisure, Essential
fitting — Regular, Slimfit, Relax, Oversize, Skinny, Slim, Boxy, Baby tee fitting — Regular, Slimfit, Relax, Oversize, Skinny, Slim, Boxy, Baby tee
......
"""
Data Retrieval Filters - Module tách biệt cho logic lọc sản phẩm.
Chứa các hàm lọc Hard/Soft Filter, maps được import từ db_field_values_mapping.
"""
import logging
import re
from agent.tools.db_field_values_mapping import (
COLOR_MAPPING,
SIMILAR_COLORS,
VALID_COLORS,
PRODUCT_NAME_KEYWORDS,
PRODUCT_TYPE_MAPPING,
PRODUCT_TYPE_ALTERNATIVES,
PRODUCT_LINE_FILTER_MAP,
GENDER_MAP,
AGE_MAP,
SLEEVE_FALLBACK_MAP,
SLEEVE_ALTERNATIVES,
SLEEVE_DISPLAY,
FIELD_MAPPINGS,
FIELD_DISPLAY_NAMES,
FIELD_ALTERNATIVES,
)
logger = logging.getLogger(__name__)
def _filter_color(products: list[dict], requested_color: str) -> tuple[list[dict], dict]:
"""
Color filter - So sánh trực tiếp với column master_color.
"""
if not requested_color or not products:
return products, {"fallback_used": False}
requested_lower = requested_color.lower().strip()
db_color = COLOR_MAPPING.get(requested_lower, requested_lower)
filtered = []
available_colors = set()
for p in products:
p_color = (p.get("master_color") or "").lower()
available_colors.add(p.get("master_color") or "Unknown")
if db_color.lower() in p_color or requested_lower in p_color:
filtered.append(p)
if filtered:
logger.info(f"✅ [COLOR] Matched '{requested_color}': {len(filtered)} products")
return filtered, {"fallback_used": False}
colors_list = sorted([c for c in available_colors if c != "Unknown"])[:5]
colors_text = ", ".join(colors_list) if colors_list else "các màu khác"
logger.warning(f"⚠️ [COLOR] No match for '{requested_color}'. Available: {colors_list}")
return products, {
"fallback_used": True,
"requested_value": requested_color,
"message": f"Shop hiện chưa có màu **{requested_color}**. Hiện có: **{colors_text}**",
"alternative_colors": colors_list,
}
# ==============================================================================
# 3. SLEEVE FALLBACK DETECTION
# ==============================================================================
def _filer_sleeve(products: list[dict], requested_sleeve: str | None) -> dict | None:
"""
Detect if products don't match the user's requested sleeve type.
Args:
products: List of products from search
requested_sleeve: User's sleeve request (e.g., "cộc tay", "dài tay")
Returns:
dict with fallback info if mismatch detected, None otherwise
"""
if not requested_sleeve or not products:
return None
requested_lower = requested_sleeve.lower().strip()
target_db_sleeve = SLEEVE_FALLBACK_MAP.get(requested_lower)
if not target_db_sleeve:
return None # Unknown sleeve type, skip detection
# Count products matching requested sleeve
matching_count = 0
other_sleeves = {} # {sleeve_type: count}
for p in products:
p_sleeve = (p.get("form_sleeve") or "").strip()
if target_db_sleeve.lower() in p_sleeve.lower():
matching_count += 1
elif p_sleeve:
other_sleeves[p_sleeve] = other_sleeves.get(p_sleeve, 0) + 1
# If all products match, no fallback needed
if matching_count == len(products):
return None
# If NO products match the requested sleeve → Fallback
if matching_count == 0 and other_sleeves:
# Get the most common alternative sleeve
top_alt = max(other_sleeves.keys(), key=lambda k: other_sleeves[k])
top_alt_display = SLEEVE_DISPLAY.get(top_alt, top_alt)
requested_display = SLEEVE_DISPLAY.get(target_db_sleeve, requested_sleeve)
return {
"fallback_used": True,
"requested_value": requested_display,
"matched_value": top_alt_display,
"message": f"Shop hiện chưa có mẫu **{requested_display}** phù hợp. Em gợi ý những mẫu **{top_alt_display}** - có thể xắn tay hoặc layer bên trong để tạo phong cách ưng ý nhé!",
"alternative_sleeves": list(other_sleeves.keys()),
}
return None
# ==============================================================================
# 6. PRODUCT TYPE FALLBACK DETECTION
# ==============================================================================
def _filter_product_name(products: list[dict], requested_product_name: str | None) -> dict | None:
"""
Detect if products don't match the user's requested product type.
Args:
products: List of products from search
requested_product_name: User's product name request (e.g., "váy liền thân")
Returns:
dict with fallback info if mismatch detected, None otherwise
"""
if not requested_product_name or not products:
return None
requested_lower = requested_product_name.lower().strip()
target_db_type = PRODUCT_TYPE_MAPPING.get(requested_lower)
if not target_db_type:
return None # Unknown product type, skip detection
# Count products matching requested type
matching_count = 0
other_types = {} # {product_line_vn: count}
for p in products:
p_type = (p.get("product_line_vn") or p.get("product_name") or "").strip()
if target_db_type.lower() in p_type.lower():
matching_count += 1
elif p_type:
# Group by first 2 words for cleaner grouping
clean_type = " ".join(p_type.split()[:3])
other_types[clean_type] = other_types.get(clean_type, 0) + 1
# If all products match, no fallback needed
if matching_count == len(products):
return None
# If NO products match the requested type → Fallback
if matching_count == 0 and other_types:
# Get the most common alternative type
top_alt = max(other_types.keys(), key=lambda k: other_types[k])
return {
"fallback_used": True,
"requested_value": requested_product_name,
"matched_value": top_alt,
"message": f"Không có {requested_product_name}. Hiển thị {top_alt} phù hợp.",
"alternative_types": list(other_types.keys())[:3],
}
return None
# ==============================================================================
# 7. GENERIC MULTI-FIELD FALLBACK DETECTION
# ==============================================================================
def _detect_field_mismatch(
products: list[dict],
field_name: str,
requested_value: str | None
) -> dict | None:
"""
Generic fallback detection for any field.
Args:
products: List of products from search
field_name: DB field name (e.g., 'style', 'fitting')
requested_value: User's requested value (Vietnamese or English)
Returns:
dict with fallback info if mismatch detected, None otherwise
"""
if not requested_value or not products or field_name not in FIELD_MAPPINGS:
return None
requested_lower = requested_value.lower().strip()
field_mapping = FIELD_MAPPINGS[field_name]
target_db_value = field_mapping.get(requested_lower)
if not target_db_value:
return None # Unknown value, skip detection
# Count products matching requested value
matching_count = 0
other_values = {}
for p in products:
p_value = (p.get(field_name) or "").strip()
if target_db_value.lower() in p_value.lower():
matching_count += 1
elif p_value:
other_values[p_value] = other_values.get(p_value, 0) + 1
# If all/most products match, no fallback needed
if matching_count >= len(products) * 0.5: # 50% threshold
return None
# If NO products match → Fallback
if matching_count == 0 and other_values:
top_alt = max(other_values.keys(), key=lambda k: other_values[k])
display_name = FIELD_DISPLAY_NAMES.get(field_name, field_name)
return {
"fallback_used": True,
"field": field_name,
"requested_value": requested_value,
"matched_value": top_alt,
"message": f"Shop hiện chưa có sản phẩm {display_name} **{requested_value}** phù hợp. Em gợi ý {display_name} **{top_alt}** - cũng rất đẹp và hợp xu hướng nhé!",
}
return None
# ==============================================================================
# 8. HELPER FUNCTIONS
# ==============================================================================
def infer_product_name_from_description(description: str | None) -> str | None:
"""
Infer product_name from description text if not explicitly provided.
Returns the first matching product type keyword found.
"""
if not description:
return None
desc_lower = description.lower()
for keyword, product_name in PRODUCT_NAME_KEYWORDS:
if keyword in desc_lower:
return product_name
return None
# ==============================================================================
# 9. MASTER POST-FILTER FUNCTION (Consolidates all filter layers)
# ==============================================================================
def apply_post_filters(
products: list[dict],
search_item, # SearchItem from data_retrieval_tool
is_sku_search: bool = False,
) -> tuple[list[dict], dict]:
"""
Master function to apply all post-filters on products.
Consolidates all filter layers:
- Layer 1.5: Gender fallback detection
- Layer 1.8: Product type filter (cascading)
- Layer 2: Color filter with smart fallback
- Layer 3: Sleeve fallback detection
- Layer 5: Multi-field fallback (style, fitting, etc.)
Args:
products: List of products from DB query
search_item: SearchItem with filter parameters
is_sku_search: If True, skip all filters (show all variants)
Returns:
Tuple of (filtered_products, filter_info_dict)
"""
original_count = len(products)
all_filter_info = {}
# ⚡ SKIP all filters when searching by SKU code
if is_sku_search:
logger.info("🎯 [SKU SEARCH] Skipping post-filters - showing all variants")
return products, {"fallback_used": False, "filters_applied": {}}
logger.info(
"🔍 [POST-FILTER] Starting with %d products. Params: product_name=%r, gender=%r, color=%r",
original_count,
search_item.product_name,
search_item.gender_by_product,
search_item.master_color,
)
# ====== LAYER 1.5: GENDER FALLBACK DETECTION ======
if search_item.gender_by_product and products:
requested_gender = search_item.gender_by_product.lower().strip()
if requested_gender in ["men", "nam", "male", "women", "nữ", "female", "nu"]:
exact_gender_count = 0
unisex_count = 0
for p in products:
p_gender = (p.get("gender_by_product") or "").lower()
if p_gender == "unisex":
unisex_count += 1
elif requested_gender in p_gender or p_gender in ["men", "nam", "women", "nữ"]:
exact_gender_count += 1
if unisex_count > 0 and exact_gender_count == 0:
gender_display = "nam" if requested_gender in ["men", "nam", "male"] else "nữ"
if gender_display == "nam":
detail_msg = "Hiện shop chưa có sản phẩm riêng dành cho **nam**. Em xin phép gợi ý những mẫu **unisex** phù hợp!"
else:
detail_msg = "Hiện shop chưa có sản phẩm riêng dành cho **nữ**. Em xin phép gợi ý những mẫu **unisex** phù hợp!"
all_filter_info["gender"] = {
"fallback_used": True,
"requested_value": gender_display,
"matched_value": "unisex",
"message": detail_msg,
}
logger.warning(f"👫 GENDER FALLBACK: '{gender_display}' → 'unisex' ({unisex_count} products)")
# ====== LAYER 1.8: PRODUCT TYPE FILTER (Cascading Fallback) ======
if search_item.product_name and products:
raw_product_names = re.split(r"[/,]", search_item.product_name)
raw_product_names = [n.strip().lower() for n in raw_product_names if n.strip()]
logger.info(f"📦 [PRODUCT TYPE] Input: '{search_item.product_name}' → Parsed: {raw_product_names}")
all_db_types = []
for pn in raw_product_names:
db_type = PRODUCT_TYPE_MAPPING.get(pn)
if db_type and db_type not in all_db_types:
all_db_types.append(db_type)
for fb in PRODUCT_TYPE_ALTERNATIVES.get(db_type, []):
if fb not in all_db_types:
all_db_types.append(fb)
if all_db_types:
before_count = len(products)
primary_types = []
for pn in raw_product_names:
db_type = PRODUCT_TYPE_MAPPING.get(pn)
if db_type and db_type not in primary_types:
primary_types.append(db_type)
matched_products = []
matched_type = None
for try_type in all_db_types:
for p in products:
p_type = (p.get("product_line_vn") or "").lower()
if try_type.lower() in p_type:
matched_products.append(p)
if matched_products:
matched_type = try_type
break
if matched_products:
products = matched_products
if matched_type not in primary_types:
detail_msg = f"Shop hiện chưa có **{search_item.product_name}** phù hợp. Em gợi ý những mẫu **{matched_type}** xinh xắn nhé!"
all_filter_info["product_type"] = {
"fallback_used": True,
"requested_value": search_item.product_name,
"matched_value": matched_type,
"message": detail_msg,
}
logger.warning(f"📦 PRODUCT TYPE FALLBACK: '{search_item.product_name}' → '{matched_type}'")
else:
logger.info(f"📦 [PRODUCT TYPE] Matched '{matched_type}': {before_count} → {len(products)}")
# ====== LAYER 2: COLOR FILTER (with smart fallback) ======
if search_item.master_color and products:
before_count = len(products)
products, color_info = _filter_color(products, search_item.master_color)
if color_info.get("fallback_used") or color_info.get("message"):
all_filter_info["color"] = color_info
if color_info.get("fallback_used"):
logger.warning(
"🎨 COLOR FALLBACK: '%s' → '%s' (%d products)",
color_info.get("requested_value"),
color_info.get("matched_value"),
len(products),
)
logger.info("🎨 Color filter: %d → %d products", before_count, len(products))
# ====== LAYER 3: SLEEVE FALLBACK DETECTION ======
if products and search_item.product_name:
sleeve_keywords = ["cộc tay", "ngắn tay", "dài tay", "sát nách", "short sleeve", "long sleeve"]
requested_sleeve = None
for kw in sleeve_keywords:
if kw in search_item.product_name.lower():
requested_sleeve = kw
break
if requested_sleeve:
sleeve_info = _detect_sleeve_mismatch(products, requested_sleeve)
if sleeve_info:
all_filter_info["sleeve"] = sleeve_info
logger.warning(
"👕 SLEEVE FALLBACK: '%s' → '%s'",
sleeve_info.get("requested_value"),
sleeve_info.get("matched_value"),
)
# ====== LAYER 5: MULTI-FIELD FALLBACK DETECTION ======
if products:
field_checks = [
("style", search_item.style),
("fitting", search_item.fitting),
("form_neckline", search_item.form_neckline),
("material_group", search_item.material_group),
("season", search_item.season),
]
for field_name, requested_val in field_checks:
if requested_val:
field_info = _detect_field_mismatch(products, field_name, requested_val)
if field_info:
all_filter_info[field_name] = field_info
logger.warning(
"🔄 %s FALLBACK: '%s' → '%s'",
field_name.upper(),
field_info.get("requested_value"),
field_info.get("matched_value"),
)
# ====== BUILD FINAL FILTER INFO ======
filter_info = {
"fallback_used": any(info.get("fallback_used") for info in all_filter_info.values()),
"filters_applied": all_filter_info,
}
fallback_messages = [info.get("message") for info in all_filter_info.values() if info.get("message")]
if fallback_messages:
filter_info["recommendation_message"] = " ".join(fallback_messages)
if original_count != len(products):
logger.info("📊 Post-filter summary: %d → %d products", original_count, len(products))
return products, filter_info
...@@ -42,6 +42,9 @@ class SearchItem(BaseModel): ...@@ -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'." "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 ====== # ====== SKU LOOKUP ======
magento_ref_code: str | None = Field( magento_ref_code: str | None = Field(
......
"""
DB FIELD VALUES - Extracted from StarRocks DB.
AI NÊN CHỈ SỬ DỤNG CÁC GIÁ TRỊ TRONG FILE NÀY!
Generated: 2026-02-05
Source: shared_source.magento_product_dimension_with_text_embedding
"""
# ============================================================
# STYLE - Phong cách sản phẩm (9 values)
# ============================================================
VALID_STYLES = [
"Basic", # 1304 products - MOST COMMON
"Dynamic", # 840 products
"Feminine", # 162 products
"Utility", # 107 products
"Basic Update", # 84 products
"Smart Casual", # 81 products
"Trend", # 14 products
"Athleisure", # 8 products
"Essential", # 3 products
]
# AI Mapping: Vietnamese → DB Value
STYLE_MAPPING = {
# Basic family
"basic": "Basic",
"cơ bản": "Basic",
"đơn giản": "Basic",
# Dynamic/Sporty
"dynamic": "Dynamic",
"năng động": "Dynamic",
"sporty": "Dynamic", # Map sporty → Dynamic
"thể thao": "Dynamic",
# Feminine/Elegant
"feminine": "Feminine",
"nữ tính": "Feminine",
"thanh lịch": "Smart Casual", # Map thanh lịch → Smart Casual
"sang trọng": "Smart Casual", # Map sang trọng → Smart Casual
"elegant": "Smart Casual", # Map elegant → Smart Casual
# Casual
"casual": "Smart Casual",
"thường ngày": "Smart Casual",
# Utility
"utility": "Utility",
"tiện dụng": "Utility",
# Trend
"trend": "Trend",
"xu hướng": "Trend",
# Athleisure
"athleisure": "Athleisure",
"active": "Athleisure",
}
# ============================================================
# MASTER_COLOR - Màu sắc chính (16 values)
# ============================================================
VALID_COLORS = [
"Đen/ Black",
"Trắng/ White",
"Xanh da trời/ Blue",
"Xám/ Gray",
"Hồng/ Pink- Magenta",
"Be/ Beige",
"Đỏ/ Red",
"Tím/ Purple",
"Xanh lá cây/ Green",
"Màu xanh Jeans",
"Xanh than/ Aqua",
"Nâu/ Brown",
"Vàng/ Yellow + Gold",
"Cam/ Orange",
]
# AI Mapping: User input → DB Value
COLOR_MAPPING = {
# Đen
"đen": "Đen/ Black",
"black": "Đen/ Black",
# Trắng
"trắng": "Trắng/ White",
"white": "Trắng/ White",
"kem": "Be/ Beige", # Kem → Be/Beige (separate!)
"cream": "Be/ Beige",
# Xanh
"xanh": "Xanh da trời/ Blue",
"blue": "Xanh da trời/ Blue",
"xanh dương": "Xanh da trời/ Blue",
"xanh lá": "Xanh lá cây/ Green",
"green": "Xanh lá cây/ Green",
"navy": "Màu xanh Jeans", # Navy → Jeans
"xanh đậm": "Màu xanh Jeans",
# Xám
"xám": "Xám/ Gray",
"gray": "Xám/ Gray",
"grey": "Xám/ Gray",
# Hồng
"hồng": "Hồng/ Pink- Magenta",
"pink": "Hồng/ Pink- Magenta",
"magenta": "Hồng/ Pink- Magenta",
# Be
"be": "Be/ Beige",
"beige": "Be/ Beige",
"nude": "Be/ Beige",
# Đỏ
"đỏ": "Đỏ/ Red",
"red": "Đỏ/ Red",
# Tím
"tím": "Tím/ Purple",
"purple": "Tím/ Purple",
# Nâu
"nâu": "Nâu/ Brown",
"brown": "Nâu/ Brown",
"chocolate": "Nâu/ Brown", # Chocolate → Brown (no compound!)
"coffee": "Nâu/ Brown",
# Vàng
"vàng": "Vàng/ Yellow + Gold",
"yellow": "Vàng/ Yellow + Gold",
"gold": "Vàng/ Yellow + Gold",
# Cam
"cam": "Cam/ Orange",
"orange": "Cam/ Orange",
}
# ============================================================
# PRODUCT_LINE_VN - Loại sản phẩm (53 values)
# ============================================================
VALID_PRODUCT_LINES_VN = [
"Áo phông",
"Quần soóc",
"Quần nỉ",
"Tất",
"Áo nỉ",
"Áo Polo",
"Bộ mặc nhà",
"Áo len",
"Bộ quần áo",
"Chân váy",
"Quần mặc nhà",
"Váy liền", # ← "Váy liền" NOT "Váy liền thân"!
"Áo Sơ mi",
"Áo nỉ có mũ",
"Áo Body",
"Quần dài",
"Quần Khaki",
"Quần jean",
"Áo khoác gió",
# ... và 34 loại khác
]
# AI Mapping: User input → DB Value
PRODUCT_LINE_MAPPING = {
# Váy family
"váy": "Váy liền", # Generic váy → Váy liền
"váy liền thân": "Váy liền", # Specific → Generic
"váy đầm": "Váy liền",
"đầm": "Váy liền",
"chân váy": "Chân váy", # Keep as is
# Áo family
"áo": "Áo phông", # Generic áo → Áo phông (most common)
"áo phông": "Áo phông",
"áo thun": "Áo phông",
"áo polo": "Áo Polo",
"áo sơ mi": "Áo Sơ mi",
"áo len": "Áo len",
"áo khoác": "Áo khoác gió", # Generic → Áo khoác gió
# Quần family
"quần": "Quần dài", # Generic quần → Quần dài
"quần jean": "Quần jean",
"quần kaki": "Quần Khaki",
"quần short": "Quần soóc",
}
# ============================================================
# FITTING - Dáng đồ (8 values)
# ============================================================
VALID_FITTINGS = [
"Regular", # 1843 products - MOST COMMON
"Slimfit", # 316 products
"Relax", # 221 products
"Oversize", # 51 products
"Skinny", # 49 products
"Slim", # 30 products
"Boxy", # 24 products
"Baby tee", # 2 products
]
FITTING_MAPPING = {
"regular": "Regular",
"vừa": "Regular",
"suông": "Regular",
"slim": "Slimfit", # Note: DB has both "Slim" and "Slimfit"
"slimfit": "Slimfit",
"ôm": "Slimfit",
"relax": "Relax",
"rộng": "Relax",
"oversize": "Oversize",
"oversized": "Oversize",
"skinny": "Skinny",
"boxy": "Boxy",
}
# ============================================================
# FORM_SLEEVE - Dáng tay áo (8 values)
# ============================================================
VALID_SLEEVES = [
"Full length Sleeve", # 701 products
"Short Sleeve", # 616 products
"Sleeveless", # 93 products
"Puff", # 6 products
"Fluffer", # 5 products
# ... others
]
SLEEVE_MAPPING = {
"dài tay": "Full length Sleeve",
"long sleeve": "Full length Sleeve",
"ngắn tay": "Short Sleeve",
"cộc tay": "Short Sleeve",
"short sleeve": "Short Sleeve",
"sát nách": "Sleeveless",
"sleeveless": "Sleeveless",
"tank": "Sleeveless",
}
# ============================================================
# GENDER & AGE
# ============================================================
VALID_GENDERS = ["women", "men", "boy", "girl", "unisex", "newborn"]
VALID_AGES = ["adult", "kid", "others"]
GENDER_MAPPING = {
"nữ": "women",
"nam": "men",
"bé trai": "boy",
"bé gái": "girl",
}
AGE_MAPPING = {
"người lớn": "adult",
"trẻ em": "kid",
}
# ============================================================
# SIMILAR COLORS - Màu gần nhau để suggest fallback
# ============================================================
# Khi không tìm thấy màu user yêu cầu, suggest các màu gần nhất
SIMILAR_COLORS = {
"Trắng/ White": ["Be/ Beige", "Xám/ Gray"],
"Be/ Beige": ["Trắng/ White", "Nâu/ Brown", "Xám/ Gray"],
"Nâu/ Brown": ["Be/ Beige", "Xám/ Gray", "Đen/ Black"],
"Xám/ Gray": ["Đen/ Black", "Trắng/ White", "Be/ Beige"],
"Đen/ Black": ["Xám/ Gray", "Nâu/ Brown"],
"Hồng/ Pink- Magenta": ["Đỏ/ Red", "Tím/ Purple"],
"Đỏ/ Red": ["Hồng/ Pink- Magenta", "Cam/ Orange"],
"Cam/ Orange": ["Đỏ/ Red", "Vàng/ Yellow + Gold"],
"Vàng/ Yellow + Gold": ["Cam/ Orange", "Be/ Beige"],
"Xanh da trời/ Blue": ["Màu xanh Jeans", "Xanh than/ Aqua"],
"Màu xanh Jeans": ["Xanh da trời/ Blue", "Xanh than/ Aqua", "Đen/ Black"],
"Xanh than/ Aqua": ["Xanh da trời/ Blue", "Màu xanh Jeans"],
"Xanh lá cây/ Green": ["Xanh da trời/ Blue", "Xanh than/ Aqua"],
"Tím/ Purple": ["Hồng/ Pink- Magenta", "Xanh da trời/ Blue"],
}
# ============================================================
# PRODUCT_NAME_KEYWORDS - Infer product type from description
# ============================================================
PRODUCT_NAME_KEYWORDS = [
("áo sơ mi", "Áo sơ mi"),
("ao so mi", "Áo sơ mi"),
("sơ mi", "Áo sơ mi"),
("so mi", "Áo sơ mi"),
("chân váy", "Chân váy"),
("chan vay", "Chân váy"),
("váy liền thân", "Váy liền thân"),
("vay lien than", "Váy liền thân"),
("váy liền", "Váy liền thân"),
("vay lien", "Váy liền thân"),
("váy đầm", "Váy liền thân"),
("vay dam", "Váy liền thân"),
("đầm", "Váy liền thân"),
("dam", "Váy liền thân"),
("váy", "Váy"),
("vay", "Váy"),
("áo khoác", "Áo khoác"),
("ao khoac", "Áo khoác"),
("áo len", "Áo len"),
("ao len", "Áo len"),
("áo thun", "Áo thun"),
("ao thun", "Áo thun"),
("áo polo", "Áo polo"),
("ao polo", "Áo polo"),
("hoodie", "Áo hoodie"),
("áo hoodie", "Áo hoodie"),
("ao hoodie", "Áo hoodie"),
("quần jeans", "Quần jeans"),
("quan jeans", "Quần jeans"),
("quần short", "Quần short"),
("quan short", "Quần short"),
("quần dài", "Quần dài"),
("quan dai", "Quần dài"),
("quần", "Quần"),
("quan", "Quần"),
("áo", "Áo"),
("ao", "Áo"),
("phụ kiện", "Phụ kiện"),
("phu kien", "Phụ kiện"),
("túi", "Túi"),
("tui", "Túi"),
("mũ", "Mũ"),
("mu", "Mũ"),
("khăn", "Khăn"),
("khan", "Khăn"),
("tất", "Tất"),
("tat", "Tất"),
]
# ============================================================
# PRODUCT_TYPE_MAPPING - Map user input to DB product_line_vn
# ============================================================
PRODUCT_TYPE_MAPPING = {
# Váy family
"váy": "Váy liền",
"vay": "Váy liền",
"váy liền thân": "Váy liền",
"váy liền": "Váy liền",
"váy đầm": "Váy liền",
"đầm": "Váy liền",
"dam": "Váy liền",
"chân váy": "Chân váy",
"chan vay": "Chân váy",
# Áo family
"áo": "Áo phông",
"ao": "Áo phông",
"áo phông": "Áo phông",
"áo thun": "Áo phông",
"ao thun": "Áo phông",
"áo polo": "Áo Polo",
"ao polo": "Áo Polo",
"áo sơ mi": "Áo Sơ mi",
"ao so mi": "Áo Sơ mi",
"sơ mi": "Áo Sơ mi",
"áo len": "Áo len",
"ao len": "Áo len",
"áo khoác": "Áo khoác gió",
"ao khoac": "Áo khoác gió",
"áo nỉ": "Áo nỉ",
"ao ni": "Áo nỉ",
"hoodie": "Áo nỉ có mũ",
"áo hoodie": "Áo nỉ có mũ",
# Quần family
"quần": "Quần dài",
"quan": "Quần dài",
"quần dài": "Quần dài",
"quan dai": "Quần dài",
"quần jean": "Quần jean",
"quan jean": "Quần jean",
"quần jeans": "Quần jean",
"quan jeans": "Quần jean",
"quần kaki": "Quần Khaki",
"quan kaki": "Quần Khaki",
"quần khaki": "Quần Khaki",
"quần short": "Quần soóc",
"quan short": "Quần soóc",
"quần soóc": "Quần soóc",
"quần đùi": "Quần soóc",
# Phụ kiện / Accessories (DB values verified 2026-02-07)
"tất": "Tất",
"vớ": "Tất",
"tat": "Tất",
"khăn quàng cổ": "Khăn", # DB: "Khăn" (15 products)
"khăn quàng": "Khăn",
"khăn choàng": "Khăn",
"khan quang co": "Khăn",
"khăn": "Khăn",
"khan": "Khăn",
"khăn mặt": "Khăn mặt", # DB: "Khăn mặt" (16 products)
"khan mat": "Khăn mặt",
"khăn tắm": "Khăn tắm", # DB: "Khăn tắm" (17 products)
"khan tam": "Khăn tắm",
"khăn lau đầu": "Khăn lau đầu", # DB: "Khăn lau đầu" (5 products)
"khan lau dau": "Khăn lau đầu",
"mũ": "Mũ", # DB: "Mũ" (16 products)
"nón": "Mũ",
"mu": "Mũ",
"non": "Mũ",
"túi xách": "Túi xách", # DB: "Túi xách" (6 products)
"tui xach": "Túi xách",
"túi": "Túi xách",
"tui": "Túi xách",
"chăn": "Chăn cá nhân", # DB: "Chăn cá nhân" (6 products)
"chan": "Chăn cá nhân",
"chăn cá nhân": "Chăn cá nhân",
"găng tay": "Găng tay chống nắng", # DB: "Găng tay chống nắng" (5 products)
"gang tay": "Găng tay chống nắng",
"găng tay chống nắng": "Găng tay chống nắng",
"phụ kiện": "Tất",
"phu kien": "Tất",
}
# ============================================================
# PRODUCT_TYPE_ALTERNATIVES - Fallback product types
# ============================================================
PRODUCT_TYPE_ALTERNATIVES = {
"Váy liền": ["Chân váy"],
"Chân váy": ["Váy liền"],
"Áo phông": ["Áo Polo", "Áo len"],
"Áo Polo": ["Áo phông", "Áo Sơ mi"],
"Áo Sơ mi": ["Áo Polo", "Áo phông"],
"Áo len": ["Áo nỉ", "Áo phông"],
"Áo nỉ": ["Áo len", "Áo nỉ có mũ"],
"Áo nỉ có mũ": ["Áo nỉ", "Áo khoác gió"],
"Áo khoác gió": ["Áo nỉ có mũ", "Áo nỉ"],
"Quần dài": ["Quần jean", "Quần Khaki"],
"Quần jean": ["Quần dài", "Quần Khaki"],
"Quần Khaki": ["Quần dài", "Quần jean"],
"Quần soóc": ["Quần dài"],
# Phụ kiện alternatives (DB values verified 2026-02-07)
"Tất": ["Khăn mặt"],
"Khăn": ["Khăn mặt", "Mũ"],
"Khăn mặt": ["Khăn tắm", "Khăn"],
"Khăn tắm": ["Khăn mặt", "Khăn lau đầu"],
"Khăn lau đầu": ["Khăn tắm", "Khăn mặt"],
"Mũ": ["Khăn", "Găng tay chống nắng"],
"Túi xách": ["Mũ"],
"Chăn cá nhân": ["Khăn tắm"],
"Găng tay chống nắng": ["Mũ"],
}
# ============================================================
# LEGACY MAPS - For backward compatibility with filter logic
# ============================================================
# Gender alternatives map (still used by apply_post_filters)
GENDER_MAP = {
"men": ["men", "nam", "male", "boy"],
"nam": ["men", "nam", "male"],
"women": ["women", "nữ", "female", "nu"],
"nữ": ["women", "nữ", "female", "nu"],
"boy": ["boy", "bé trai", "be trai"],
"bé trai": ["boy", "bé trai", "be trai"],
"girl": ["girl", "bé gái", "be gai"],
"bé gái": ["girl", "bé gái", "be gai"],
"unisex": ["unisex"],
}
# Age alternatives map (still used by apply_post_filters)
AGE_MAP = {
"adult": ["adult", "người lớn", "nguoi lon"],
"người lớn": ["adult", "người lớn", "nguoi lon"],
"kid": ["kid", "trẻ em", "tre em", "kids", "child", "children"],
"trẻ em": ["kid", "trẻ em", "tre em", "kids"],
}
# ============================================================
# PRODUCT_LINE_FILTER_MAP - For post-filter product line matching
# Maps user search term -> acceptable product_line values
# ============================================================
PRODUCT_LINE_FILTER_MAP = {
# Chân váy / Skirt
"chân váy": ["skirt", "chân váy", "chan vay"],
"chan vay": ["skirt", "chân váy", "chan vay"],
"skirt": ["skirt", "chân váy"],
# Váy / Dress
"váy": ["dress", "váy", "vay", "đầm", "dam"],
"vay": ["dress", "váy", "vay"],
"dress": ["dress", "váy", "đầm"],
"đầm": ["dress", "đầm", "dam", "váy"],
"dam": ["dress", "đầm", "dam"],
"váy liền thân": ["dress", "váy", "vay", "đầm", "dam"],
"vay lien than": ["dress", "váy", "vay", "đầm", "dam"],
"đầm liền thân": ["dress", "váy", "vay", "đầm", "dam"],
"dam lien than": ["dress", "váy", "vay", "đầm", "dam"],
"váy liền": ["dress", "váy", "vay", "đầm", "dam"],
"vay lien": ["dress", "váy", "vay", "đầm", "dam"],
"váy đầm": ["dress", "váy", "vay", "đầm", "dam"],
"vay dam": ["dress", "váy", "vay", "đầm", "dam"],
"váy bé gái": ["dress", "váy", "vay", "đầm", "dam"],
"vay be gai": ["dress", "váy", "vay", "đầm", "dam"],
"chân váy bé gái": ["skirt", "chân váy", "chan vay"],
"chan vay be gai": ["skirt", "chân váy", "chan vay"],
# Áo / Shirt / Top
"áo": ["shirt", "top", "polo", "t-shirt", "blouse", "áo", "ao"],
"ao": ["shirt", "top", "áo", "ao"],
"áo sơ mi": ["shirt", "áo sơ mi", "ao so mi"],
"ao so mi": ["shirt", "áo sơ mi"],
"áo thun": ["t-shirt", "tee", "áo thun", "ao thun"],
"ao thun": ["t-shirt", "áo thun"],
"áo polo": ["polo", "áo polo", "ao polo"],
"ao polo": ["polo", "áo polo"],
"áo khoác": ["jacket", "coat", "áo khoác", "ao khoac", "outerwear"],
"ao khoac": ["jacket", "coat", "áo khoác"],
"áo len": ["sweater", "knit", "knitwear", "áo len", "ao len"],
"ao len": ["sweater", "knit", "áo len"],
"áo hoodie": ["hoodie", "áo hoodie"],
"hoodie": ["hoodie", "áo hoodie"],
"shirt": ["shirt", "áo", "ao"],
"t-shirt": ["t-shirt", "tee", "áo thun"],
"top": ["top", "áo", "blouse"],
# Quần / Pants
"quần": ["pants", "trousers", "shorts", "jeans", "quần", "quan"],
"quan": ["pants", "trousers", "quần", "quan"],
"quần jeans": ["jeans", "denim", "quần jeans", "quan jeans"],
"quan jeans": ["jeans", "quần jeans"],
"jeans": ["jeans", "denim", "quần jeans"],
"quần short": ["shorts", "quần short", "quan short"],
"quan short": ["shorts", "quần short"],
"shorts": ["shorts", "quần short"],
"quần dài": ["pants", "trousers", "quần dài", "quan dai"],
"quan dai": ["pants", "trousers", "quần dài"],
"pants": ["pants", "trousers", "quần"],
"trousers": ["pants", "trousers", "quần"],
# Bộ / Set
"bộ": ["set", "bộ", "bo", "bộ quần áo"],
"bo": ["set", "bộ", "bo"],
"bộ quần áo": ["set", "bộ quần áo", "bo quan ao"],
"bo quan ao": ["set", "bộ quần áo"],
"set": ["set", "bộ"],
# Phụ kiện / Accessories
"phụ kiện": ["accessory", "accessories", "phụ kiện", "phu kien"],
"phu kien": ["accessory", "phụ kiện"],
"accessory": ["accessory", "accessories", "phụ kiện"],
"túi": ["bag", "túi", "tui"],
"tui": ["bag", "túi"],
"bag": ["bag", "túi"],
"mũ": ["hat", "cap", "mũ", "mu", "nón"],
"mu": ["hat", "cap", "mũ"],
"hat": ["hat", "cap", "mũ"],
"cap": ["cap", "hat", "mũ"],
"khăn": ["scarf", "khăn", "khan"],
"khan": ["scarf", "khăn"],
"scarf": ["scarf", "khăn"],
# Tất / Socks
"tất": ["socks", "tất", "tat"],
"tat": ["socks", "tất"],
"socks": ["socks", "tất"],
}
# ============================================================
# SLEEVE MAPPINGS - For post-filter sleeve matching
# ============================================================
# Mapping user input → DB form_sleeve value
SLEEVE_FALLBACK_MAP = {
"cộc tay": "Short Sleeve",
"ngắn tay": "Short Sleeve",
"short sleeve": "Short Sleeve",
"dài tay": "Full length Sleeve",
"long sleeve": "Full length Sleeve",
"sát nách": "Sleeveless",
"sleeveless": "Sleeveless",
"tank": "Sleeveless",
}
# Opposite sleeve for fallback suggestion
SLEEVE_ALTERNATIVES = {
"Short Sleeve": ["Full length Sleeve", "Sleeveless"],
"Full length Sleeve": ["Short Sleeve"],
"Sleeveless": ["Short Sleeve"],
}
# Display names for user messages
SLEEVE_DISPLAY = {
"Short Sleeve": "cộc tay",
"Full length Sleeve": "dài tay",
"Sleeveless": "sát nách",
}
# ============================================================
# FIELD MAPPINGS - For generic multi-field fallback detection
# ============================================================
FIELD_MAPPINGS = {
"style": {
"basic": "Basic",
"cơ bản": "Basic",
"dynamic": "Dynamic",
"năng động": "Dynamic",
"sporty": "Dynamic",
"thể thao": "Dynamic",
"feminine": "Feminine",
"nữ tính": "Feminine",
"smart casual": "Smart Casual",
"thanh lịch": "Smart Casual",
"casual": "Smart Casual",
},
"fitting": {
"regular": "Regular",
"vừa": "Regular",
"suông": "Regular",
"slimfit": "Slimfit",
"slim": "Slimfit",
"ôm": "Slimfit",
"relax": "Relax",
"rộng": "Relax",
"oversize": "Oversize",
"skinny": "Skinny",
"boxy": "Boxy",
},
"form_neckline": {
"cổ tròn": "Crew Neck",
"crew neck": "Crew Neck",
"cổ chữ v": "V-neck",
"v-neck": "V-neck",
"cổ bẻ": "Classic Collar",
"cổ đức": "Mock Neck/ High neck",
"cổ cao": "Mock Neck/ High neck",
"có mũ": "Hooded collar",
"hoodie": "Hooded collar",
},
"material_group": {
"dệt kim": "Knit - Dệt Kim",
"knit": "Knit - Dệt Kim",
"dệt thoi": "Woven - Dệt Thoi",
"woven": "Woven - Dệt Thoi",
"len": "Yarn - Sợi",
"sợi": "Yarn - Sợi",
},
"season": {
"thu đông": "Fall Winter",
"mùa đông": "Fall Winter",
"fall winter": "Fall Winter",
"xuân hè": "Spring Summer",
"mùa hè": "Spring Summer",
"spring summer": "Spring Summer",
"4 mùa": "Year",
"quanh năm": "Year",
},
"form_length": {
"dài": "Long",
"long": "Long",
"ngắn": "Short",
"short": "Short",
"midi": "Midi",
"mini": "Mini",
"maxi": "Maxi",
},
}
# Display names for user-friendly messages
FIELD_DISPLAY_NAMES = {
"style": "phong cách",
"fitting": "dáng",
"form_neckline": "kiểu cổ",
"material_group": "chất liệu",
"season": "mùa",
"form_length": "độ dài",
}
# Alternative values for fallback suggestions
FIELD_ALTERNATIVES = {
"style": {
"Basic": ["Dynamic", "Smart Casual"],
"Dynamic": ["Basic", "Athleisure"],
"Feminine": ["Smart Casual", "Trend"],
"Smart Casual": ["Basic", "Feminine"],
},
"fitting": {
"Slimfit": ["Slim", "Regular"],
"Slim": ["Slimfit", "Regular"],
"Regular": ["Relax", "Slimfit"],
"Oversize": ["Relax", "Boxy"],
"Relax": ["Regular", "Oversize"],
},
"season": {
"Fall Winter": ["Year"],
"Spring Summer": ["Year"],
"Year": ["Fall Winter", "Spring Summer"],
},
}
"""
Data Retrieval Filters - Module tách biệt cho logic lọc sản phẩm.
Chứa các hàm lọc Hard/Soft Filter, maps được import từ db_field_values_mapping.
"""
import logging
import re
from agent.tools.db_field_values_mapping import (
COLOR_MAPPING,
SIMILAR_COLORS,
VALID_COLORS,
PRODUCT_NAME_KEYWORDS,
PRODUCT_TYPE_MAPPING,
PRODUCT_TYPE_ALTERNATIVES,
PRODUCT_LINE_FILTER_MAP,
GENDER_MAP,
AGE_MAP,
SLEEVE_FALLBACK_MAP,
SLEEVE_ALTERNATIVES,
SLEEVE_DISPLAY,
FIELD_MAPPINGS,
FIELD_DISPLAY_NAMES,
FIELD_ALTERNATIVES,
)
logger = logging.getLogger(__name__)
def _filter_color(products: list[dict], requested_color: str) -> tuple[list[dict], dict]:
"""
Color filter - So sánh trực tiếp với column master_color.
"""
if not requested_color or not products:
return products, {"fallback_used": False}
requested_lower = requested_color.lower().strip()
db_color = COLOR_MAPPING.get(requested_lower, requested_lower)
filtered = []
available_colors = set()
for p in products:
p_color = (p.get("master_color") or "").lower()
available_colors.add(p.get("master_color") or "Unknown")
if db_color.lower() in p_color or requested_lower in p_color:
filtered.append(p)
if filtered:
logger.info(f"✅ [COLOR] Matched '{requested_color}': {len(filtered)} products")
return filtered, {"fallback_used": False}
colors_list = sorted([c for c in available_colors if c != "Unknown"])[:5]
colors_text = ", ".join(colors_list) if colors_list else "các màu khác"
logger.warning(f"⚠️ [COLOR] No match for '{requested_color}'. Available: {colors_list}")
return products, {
"fallback_used": True,
"requested_value": requested_color,
"message": f"Shop hiện chưa có màu **{requested_color}**. Hiện có: **{colors_text}**",
"alternative_colors": colors_list,
}
# ==============================================================================
# 3. SLEEVE FALLBACK DETECTION
# ==============================================================================
def _detect_sleeve_mismatch(products: list[dict], requested_sleeve: str | None) -> dict | None:
"""
Detect if products don't match the user's requested sleeve type.
Args:
products: List of products from search
requested_sleeve: User's sleeve request (e.g., "cộc tay", "dài tay")
Returns:
dict with fallback info if mismatch detected, None otherwise
"""
if not requested_sleeve or not products:
return None
requested_lower = requested_sleeve.lower().strip()
target_db_sleeve = SLEEVE_FALLBACK_MAP.get(requested_lower)
if not target_db_sleeve:
return None # Unknown sleeve type, skip detection
# Count products matching requested sleeve
matching_count = 0
other_sleeves = {} # {sleeve_type: count}
for p in products:
p_sleeve = (p.get("form_sleeve") or "").strip()
if target_db_sleeve.lower() in p_sleeve.lower():
matching_count += 1
elif p_sleeve:
other_sleeves[p_sleeve] = other_sleeves.get(p_sleeve, 0) + 1
# If all products match, no fallback needed
if matching_count == len(products):
return None
# If NO products match the requested sleeve → Fallback
if matching_count == 0 and other_sleeves:
# Get the most common alternative sleeve
top_alt = max(other_sleeves.keys(), key=lambda k: other_sleeves[k])
top_alt_display = SLEEVE_DISPLAY.get(top_alt, top_alt)
requested_display = SLEEVE_DISPLAY.get(target_db_sleeve, requested_sleeve)
return {
"fallback_used": True,
"requested_value": requested_display,
"matched_value": top_alt_display,
"message": f"Shop hiện chưa có mẫu **{requested_display}** phù hợp. Em gợi ý những mẫu **{top_alt_display}** - có thể xắn tay hoặc layer bên trong để tạo phong cách ưng ý nhé!",
"alternative_sleeves": list(other_sleeves.keys()),
}
return None
# ==============================================================================
# 6. PRODUCT TYPE FALLBACK DETECTION
# ==============================================================================
def _detect_product_type_mismatch(products: list[dict], requested_product_name: str | None) -> dict | None:
"""
Detect if products don't match the user's requested product type.
Args:
products: List of products from search
requested_product_name: User's product name request (e.g., "váy liền thân")
Returns:
dict with fallback info if mismatch detected, None otherwise
"""
if not requested_product_name or not products:
return None
requested_lower = requested_product_name.lower().strip()
target_db_type = PRODUCT_TYPE_MAPPING.get(requested_lower)
if not target_db_type:
return None # Unknown product type, skip detection
# Count products matching requested type
matching_count = 0
other_types = {} # {product_line_vn: count}
for p in products:
p_type = (p.get("product_line_vn") or p.get("product_name") or "").strip()
if target_db_type.lower() in p_type.lower():
matching_count += 1
elif p_type:
# Group by first 2 words for cleaner grouping
clean_type = " ".join(p_type.split()[:3])
other_types[clean_type] = other_types.get(clean_type, 0) + 1
# If all products match, no fallback needed
if matching_count == len(products):
return None
# If NO products match the requested type → Fallback
if matching_count == 0 and other_types:
# Get the most common alternative type
top_alt = max(other_types.keys(), key=lambda k: other_types[k])
return {
"fallback_used": True,
"requested_value": requested_product_name,
"matched_value": top_alt,
"message": f"Không có {requested_product_name}. Hiển thị {top_alt} phù hợp.",
"alternative_types": list(other_types.keys())[:3],
}
return None
# ==============================================================================
# 7. GENERIC MULTI-FIELD FALLBACK DETECTION
# ==============================================================================
def _detect_field_mismatch(
products: list[dict],
field_name: str,
requested_value: str | None
) -> dict | None:
"""
Generic fallback detection for any field.
Args:
products: List of products from search
field_name: DB field name (e.g., 'style', 'fitting')
requested_value: User's requested value (Vietnamese or English)
Returns:
dict with fallback info if mismatch detected, None otherwise
"""
if not requested_value or not products or field_name not in FIELD_MAPPINGS:
return None
requested_lower = requested_value.lower().strip()
field_mapping = FIELD_MAPPINGS[field_name]
target_db_value = field_mapping.get(requested_lower)
if not target_db_value:
return None # Unknown value, skip detection
# Count products matching requested value
matching_count = 0
other_values = {}
for p in products:
p_value = (p.get(field_name) or "").strip()
if target_db_value.lower() in p_value.lower():
matching_count += 1
elif p_value:
other_values[p_value] = other_values.get(p_value, 0) + 1
# If all/most products match, no fallback needed
if matching_count >= len(products) * 0.5: # 50% threshold
return None
# If NO products match → Fallback
if matching_count == 0 and other_values:
top_alt = max(other_values.keys(), key=lambda k: other_values[k])
display_name = FIELD_DISPLAY_NAMES.get(field_name, field_name)
return {
"fallback_used": True,
"field": field_name,
"requested_value": requested_value,
"matched_value": top_alt,
"message": f"Shop hiện chưa có sản phẩm {display_name} **{requested_value}** phù hợp. Em gợi ý {display_name} **{top_alt}** - cũng rất đẹp và hợp xu hướng nhé!",
}
return None
# ==============================================================================
# 8. HELPER FUNCTIONS
# ==============================================================================
def infer_product_name_from_description(description: str | None) -> str | None:
"""
Infer product_name from description text if not explicitly provided.
Returns the first matching product type keyword found.
"""
if not description:
return None
desc_lower = description.lower()
for keyword, product_name in PRODUCT_NAME_KEYWORDS:
if keyword in desc_lower:
return product_name
return None
# ==============================================================================
# 9. MASTER POST-FILTER FUNCTION (Consolidates all filter layers)
# ==============================================================================
def apply_post_filters(
products: list[dict],
search_item, # SearchItem from data_retrieval_tool
is_sku_search: bool = False,
) -> tuple[list[dict], dict]:
"""
Master function to apply all post-filters on products.
Consolidates all filter layers:
- Layer 1.5: Gender fallback detection
- Layer 1.8: Product type filter (cascading)
- Layer 2: Color filter with smart fallback
- Layer 3: Sleeve fallback detection
- Layer 5: Multi-field fallback (style, fitting, etc.)
Args:
products: List of products from DB query
search_item: SearchItem with filter parameters
is_sku_search: If True, skip all filters (show all variants)
Returns:
Tuple of (filtered_products, filter_info_dict)
"""
original_count = len(products)
all_filter_info = {}
# ⚡ SKIP all filters when searching by SKU code
if is_sku_search:
logger.info("🎯 [SKU SEARCH] Skipping post-filters - showing all variants")
return products, {"fallback_used": False, "filters_applied": {}}
logger.info(
"🔍 [POST-FILTER] Starting with %d products. Params: product_name=%r, gender=%r, color=%r",
original_count,
search_item.product_name,
search_item.gender_by_product,
search_item.master_color,
)
# ====== LAYER 1.5: GENDER FALLBACK DETECTION ======
if search_item.gender_by_product and products:
requested_gender = search_item.gender_by_product.lower().strip()
if requested_gender in ["men", "nam", "male", "women", "nữ", "female", "nu"]:
exact_gender_count = 0
unisex_count = 0
for p in products:
p_gender = (p.get("gender_by_product") or "").lower()
if p_gender == "unisex":
unisex_count += 1
elif requested_gender in p_gender or p_gender in ["men", "nam", "women", "nữ"]:
exact_gender_count += 1
if unisex_count > 0 and exact_gender_count == 0:
gender_display = "nam" if requested_gender in ["men", "nam", "male"] else "nữ"
if gender_display == "nam":
detail_msg = "Hiện shop chưa có sản phẩm riêng dành cho **nam**. Em xin phép gợi ý những mẫu **unisex** phù hợp!"
else:
detail_msg = "Hiện shop chưa có sản phẩm riêng dành cho **nữ**. Em xin phép gợi ý những mẫu **unisex** phù hợp!"
all_filter_info["gender"] = {
"fallback_used": True,
"requested_value": gender_display,
"matched_value": "unisex",
"message": detail_msg,
}
logger.warning(f"👫 GENDER FALLBACK: '{gender_display}' → 'unisex' ({unisex_count} products)")
# ====== LAYER 1.8: PRODUCT TYPE FILTER (Cascading Fallback) ======
if search_item.product_name and products:
raw_product_names = re.split(r"[/,]", search_item.product_name)
raw_product_names = [n.strip().lower() for n in raw_product_names if n.strip()]
logger.info(f"📦 [PRODUCT TYPE] Input: '{search_item.product_name}' → Parsed: {raw_product_names}")
all_db_types = []
for pn in raw_product_names:
db_type = PRODUCT_TYPE_MAPPING.get(pn)
if db_type and db_type not in all_db_types:
all_db_types.append(db_type)
for fb in PRODUCT_TYPE_ALTERNATIVES.get(db_type, []):
if fb not in all_db_types:
all_db_types.append(fb)
if all_db_types:
before_count = len(products)
primary_types = []
for pn in raw_product_names:
db_type = PRODUCT_TYPE_MAPPING.get(pn)
if db_type and db_type not in primary_types:
primary_types.append(db_type)
matched_products = []
matched_type = None
for try_type in all_db_types:
for p in products:
p_type = (p.get("product_line_vn") or "").lower()
if try_type.lower() in p_type:
matched_products.append(p)
if matched_products:
matched_type = try_type
break
if matched_products:
products = matched_products
if matched_type not in primary_types:
detail_msg = f"Shop hiện chưa có **{search_item.product_name}** phù hợp. Em gợi ý những mẫu **{matched_type}** xinh xắn nhé!"
all_filter_info["product_type"] = {
"fallback_used": True,
"requested_value": search_item.product_name,
"matched_value": matched_type,
"message": detail_msg,
}
logger.warning(f"📦 PRODUCT TYPE FALLBACK: '{search_item.product_name}' → '{matched_type}'")
else:
logger.info(f"📦 [PRODUCT TYPE] Matched '{matched_type}': {before_count} → {len(products)}")
# ====== LAYER 2: COLOR FILTER (with smart fallback) ======
if search_item.master_color and products:
before_count = len(products)
products, color_info = _filter_color(products, search_item.master_color)
if color_info.get("fallback_used") or color_info.get("message"):
all_filter_info["color"] = color_info
if color_info.get("fallback_used"):
logger.warning(
"🎨 COLOR FALLBACK: '%s' → '%s' (%d products)",
color_info.get("requested_value"),
color_info.get("matched_value"),
len(products),
)
logger.info("🎨 Color filter: %d → %d products", before_count, len(products))
# ====== LAYER 3: SLEEVE FALLBACK DETECTION ======
if products and search_item.product_name:
sleeve_keywords = ["cộc tay", "ngắn tay", "dài tay", "sát nách", "short sleeve", "long sleeve"]
requested_sleeve = None
for kw in sleeve_keywords:
if kw in search_item.product_name.lower():
requested_sleeve = kw
break
if requested_sleeve:
sleeve_info = _detect_sleeve_mismatch(products, requested_sleeve)
if sleeve_info:
all_filter_info["sleeve"] = sleeve_info
logger.warning(
"👕 SLEEVE FALLBACK: '%s' → '%s'",
sleeve_info.get("requested_value"),
sleeve_info.get("matched_value"),
)
# ====== LAYER 5: MULTI-FIELD FALLBACK DETECTION ======
if products:
field_checks = [
("style", search_item.style),
("fitting", search_item.fitting),
("form_neckline", search_item.form_neckline),
("material_group", search_item.material_group),
("season", search_item.season),
]
for field_name, requested_val in field_checks:
if requested_val:
field_info = _detect_field_mismatch(products, field_name, requested_val)
if field_info:
all_filter_info[field_name] = field_info
logger.warning(
"🔄 %s FALLBACK: '%s' → '%s'",
field_name.upper(),
field_info.get("requested_value"),
field_info.get("matched_value"),
)
# ====== BUILD FINAL FILTER INFO ======
filter_info = {
"fallback_used": any(info.get("fallback_used") for info in all_filter_info.values()),
"filters_applied": all_filter_info,
}
fallback_messages = [info.get("message") for info in all_filter_info.values() if info.get("message")]
if fallback_messages:
filter_info["recommendation_message"] = " ".join(fallback_messages)
if original_count != len(products):
logger.info("📊 Post-filter summary: %d → %d products", original_count, len(products))
return products, filter_info
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! 🚀
-- [HYDE SEARCH] Full Executable Query
-- Original Params: ['women', 'unisex', 'adult', '%tím%', '%tím%', '%liền%']
WITH vector_matches AS (
SELECT /*+ SET_VAR(ann_params='{"ef_search":256}') */
internal_ref_code,
magento_ref_code,
product_color_code,
product_name,
master_color,
product_color_name,
product_image_url_thumbnail,
product_web_url,
sale_price,
original_price,
discount_amount,
ROUND(((original_price - sale_price) / original_price * 100), 0) as discount_percent,
age_by_product,
gender_by_product,
product_line_vn,
product_line_en,
description_text_full,
quantity_sold,
is_new_product,
approx_cosine_similarity(vector, [0.041171539574861526,0.03616797924041748,-0.04494577646255493,-0.028166595846414566,0.03670715540647507,-0.004836415871977806,0.005936336703598499,0.04503204673528671,0.06521882861852646,-0.014870496466755867,-0.050294410437345505,0.028080327436327934,-0.013997029513120651,-0.03474455326795578,0.02368064410984516,0.04800830036401749,-0.0194642823189497,0.002722842851653695,-0.030862480401992798,0.030129199847579002,0.013889194466173649,0.015280270017683506,0.012573602609336376,0.011214877478778362,0.018288876861333847,0.058317363262176514,0.03536999970674515,0.004480559378862381,0.02178274281322956,-0.03431321308016777,0.03142322227358818,-0.034528881311416626,0.024478627368807793,-0.05723900720477104,0.04498891159892082,0.021351400762796402,0.05051008239388466,0.01314513012766838,0.01698406971991062,-0.01670369692146778,0.025406010448932648,-0.00490111717954278,0.042249895632267,-0.009322366677224636,0.031078150495886803,0.029395919293165207,-0.0014369061682373285,0.0006557737942785025,0.021189648658037186,-0.003307849634438753,0.00994781218469143,-0.039942216128110886,-0.011171743273735046,0.043694887310266495,-0.02016521245241165,-0.00941941887140274,0.004868766292929649,0.011948158033192158,0.03028016909956932,-0.01120409369468689,0.007839630357921124,-0.04921605810523033,-0.027519583702087402,-0.0007036257302388549,-0.02976255863904953,-0.010810494422912598,-0.027929358184337616,-0.023184603080153465,0.0034857778809964657,-0.01089137140661478,0.04736128821969032,0.018051639199256897,-0.03317015618085861,0.02249445579946041,-0.006081914063543081,0.05473722890019417,-0.04268123582005501,-0.02639809623360634,-0.01051394734531641,-0.04076176509261131,-0.043177276849746704,0.030689943581819534,0.005345937795937061,0.03916580229997635,-0.03914423659443855,-0.02956845611333847,-0.027929358184337616,0.06478748470544815,0.014158782549202442,0.021394535899162292,0.0067612770944833755,-0.006513255648314953,0.04498891159892082,-0.005383680108934641,0.025406010448932648,-0.02506093680858612,0.0016364016337320209,-0.040222588926553726,-0.026354961097240448,0.043543919920921326,0.042789071798324585,-0.005688315257430077,0.035671938210725784,-0.013802926056087017,0.023249303922057152,-0.037720806896686554,0.013986245729029179,-0.020229913294315338,-0.05374514311552048,0.002678360790014267,-0.058878105133771896,-0.00401147548109293,-0.01755559630692005,0.042508698999881744,0.019496632739901543,0.025837352499365807,0.04268123582005501,0.03317015618085861,-0.023853180930018425,-0.03090561367571354,-0.012454983778297901,-0.005132962949573994,0.02288266271352768,-0.06056033819913864,-0.0351111926138401,0.04138721153140068,-0.011430548503994942,0.026031455025076866,-0.003046348923817277,-0.03493865579366684,-0.013846060261130333,0.04524771496653557,0.005273148883134127,-0.00780727993696928,0.010066431015729904,-0.017361493781208992,-0.051329631358385086,0.08014323562383652,-0.04593786224722862,0.008341064676642418,0.06780687719583511,-0.011053124442696571,0.05490976572036743,0.029870394617319107,0.011549167335033417,0.04503204673528671,-0.0019059899495914578,0.020801441743969917,-0.024953102692961693,0.05158843472599983,-0.0012994160642847419,0.004903812892735004,0.0035477832425385714,0.01065952517092228,-0.04106370359659195,-0.019755437970161438,-0.009139047004282475,0.0066642253659665585,-0.04641233757138252,0.018968239426612854,0.01618608832359314,-0.023723779246211052,0.011635434813797474,-0.027519583702087402,-0.032307472079992294,-0.0021391839254647493,0.018482981249690056,-0.014083297923207283,-0.05046694725751877,0.014514639042317867,0.005855460185557604,-0.011754054576158524,-0.008006775751709938,0.016779182478785515,-0.0002788554993458092,-0.03478768840432167,0.020758306607604027,0.015366538427770138,-0.0327603816986084,0.057454679161310196,0.04671427607536316,0.02197684533894062,-0.0028792039956897497,-0.06327778846025467,-0.08199800550937653,-0.0022483672946691513,-0.03972654789686203,0.01618608832359314,0.029439052566885948,-0.02368064410984516,-0.022580724209547043,0.010233575478196144,0.037569839507341385,-0.030107632279396057,0.025664815679192543,-0.05258052051067352,-0.012929460033774376,0.004774410743266344,0.030776211991906166,-0.011322712525725365,0.008702313527464867,-0.003612484550103545,-0.013899978250265121,-0.029201814904808998,0.021577855572104454,-0.03155262768268585,-0.06715986132621765,0.012627520598471165,0.05710960552096367,-0.02368064410984516,0.053615741431713104,0.005893202498555183,-0.001353333704173565,-0.02007894404232502,0.034485746175050735,-0.02700197324156761,0.04878471791744232,-0.004936163779348135,0.00414087763056159,0.02378848008811474,0.000368662120308727,0.009095912799239159,0.03066837601363659,0.029503753408789635,-0.01618608832359314,-0.003448035567998886,0.06392480432987213,-0.000943559396546334,-0.06940283626317978,-0.03470141813158989,-0.00039932780782692134,0.03834625333547592,0.017641864717006683,-0.028792040422558784,-0.021707257255911827,-0.018526114523410797,-0.019237827509641647,-0.006858328823000193,0.046110399067401886,0.05288245901465416,-0.027541151270270348,-0.0313153900206089,-0.011355062946677208,0.012012858875095844,0.0057584079913794994,-0.00911747943609953,-0.019518200308084488,-0.022300351411104202,0.03217807039618492,0.01614295318722725,0.062156300991773605,0.0035531751345843077,-0.06728926301002502,-0.021858226507902145,-0.020909275859594345,0.0073004537262022495,-0.04900038614869118,-0.006459338124841452,0.013943111523985863,-0.01452542282640934,0.04598099738359451,0.004445512779057026,-0.01018504984676838,-0.006200533360242844,0.00959195476025343,-0.016789965331554413,0.02790779061615467,0.07246536016464233,0.01089137140661478,0.04688681289553642,-0.0189898069947958,-0.026743169873952866,0.011635434813797474,0.04912979155778885,-0.01822417601943016,-0.012724572792649269,-0.010093390010297298,0.008572910912334919,0.039899084717035294,0.05370200797915459,-0.03858349099755287,-0.02169647440314293,0.043414514511823654,-0.031164418905973434,0.05288245901465416,0.04619666934013367,0.025190340355038643,-0.019572118297219276,0.0057314494624733925,-0.021610205993056297,-0.011829539202153683,0.02478056587278843,-0.04380272328853607,-0.03248000890016556,0.025039371103048325,0.007758754305541515,-0.026807870715856552,-0.011096258647739887,0.0034965614322572947,0.010400720871984959,0.038756027817726135,0.042228326201438904,0.003795804688706994,-0.007650918792933226,0.009759100154042244,0.0030409570317715406,0.0215994231402874,-0.049302324652671814,0.025880485773086548,-0.011754054576158524,0.02060733735561371,0.018957456573843956,-0.019345663487911224,-0.04498891159892082,-0.03157419338822365,-0.004636920522898436,-0.08324889838695526,0.01746932789683342,0.030409570783376694,0.003229669062420726,0.02577265165746212,0.023184603080153465,0.0598270557820797,0.005882418714463711,0.03198396787047386,0.007456814870238304,0.03213493525981903,-0.006577956955879927,0.03873446211218834,-0.032156504690647125,-0.06866955757141113,-0.025082504376769066,-0.011958940885961056,0.011452115140855312,0.011754054576158524,-0.015150868333876133,0.025902053341269493,0.010282101109623909,0.025147205218672752,0.040459826588630676,0.031725164502859116,-0.05184724181890488,-0.049302324652671814,0.007133308798074722,0.024241389706730843,0.010071822442114353,0.04043826088309288,-0.027994059026241302,-0.036858126521110535,0.02790779061615467,0.0030301737133413553,-0.0028899875469505787,-0.017027202993631363,0.030603675171732903,-0.01575474627315998,-0.03317015618085861,0.0654776319861412,-0.004844503477215767,-0.07759832590818405,0.05279619246721268,-0.016811532899737358,-0.07130073755979538,0.012023642659187317,-0.008966510184109211,-0.01065413374453783,-0.01978778839111328,0.05685080215334892,0.020855357870459557,0.010050255805253983,0.011408980935811996,0.018429063260555267,0.05736841261386871,0.008195486851036549,0.012454983778297901,-0.006815194617956877,0.060258399695158005,0.023982584476470947,-0.008669963106513023,0.02572951652109623,-0.0417754165828228,0.018148690462112427,-0.023055199533700943,-0.026139291003346443,0.018450630828738213,-0.0024856049567461014,-0.0013620953541249037,-0.003156880149617791,0.00020505567954387516,0.07440640032291412,-0.002927730092778802,-0.012994160875678062,-0.05046694725751877,0.029740992933511734,0.041840121150016785,-0.015431240200996399,-0.022904230281710625,0.09946733713150024,0.01288632582873106,-0.024047285318374634,-0.02991352789103985,0.024694297462701797,0.02183666080236435,-0.019280962646007538,0.010486988350749016,0.02948218770325184,0.012142261490225792,0.016595860943198204,-0.007931291125714779,0.012476551346480846,0.024435492232441902,0.0023575504310429096,-0.006081914063543081,0.037612974643707275,0.00975370779633522,0.022429754957556725,-0.021092595532536507,0.03554253280162811,0.0011949505424126983,-0.02098476141691208,0.009683615528047085,-0.013716657646000385,-0.01812712475657463,-0.0185153316706419,-0.011807971633970737,0.04050296172499657,0.03711692988872528,0.025406010448932648,-0.0016431412659585476,0.008540560491383076,-0.05870556831359863,0.0038443305529654026,-0.06797941029071808,0.04671427607536316,-0.017598731443285942,0.01267065480351448,-0.036771856248378754,-0.0002589733630884439,0.08005697280168533,0.0313153900206089,0.010983031243085861,0.02473743073642254,0.01072961837053299,-0.01497833151370287,-0.00010160113015444949,0.04102057218551636,0.00812000222504139,0.044730108231306076,-0.0051410505548119545,-0.03498179093003273,0.011667786166071892,-0.02462959662079811,-0.010336019098758698,-0.016811532899737358,-0.03459358215332031,0.012002075091004372,0.051286496222019196,-0.013511770404875278,-0.04086960107088089,-0.01836436241865158,-0.01338236778974533,0.058878105133771896,-0.007893548347055912,0.0313369557261467,0.010481596924364567,0.013511770404875278,0.0021728824358433485,-0.01841827854514122,-0.03976967930793762,0.025233473628759384,0.01359803881496191,-0.02277482859790325,-0.018774136900901794,0.03910110145807266,-0.012476551346480846,0.0038982483092695475,0.038518790155649185,0.043738022446632385,0.02387474849820137,-0.02331400476396084,-0.019539767876267433,-0.010848237201571465,0.03976967930793762,-0.022106248885393143,-0.034722987562417984,0.02007894404232502,0.0773826539516449,0.05542737618088722,0.028360700234770775,-0.04268123582005501,0.03437791392207146,-0.007456814870238304,0.015398888848721981,-0.005370201077312231,-0.008146961219608784,0.004822936374694109,-0.002352158771827817,0.01089137140661478,-0.0013000901089981198,0.005343242082744837,-0.050337545573711395,-0.006119656842201948,-0.033385828137397766,0.034572016447782516,-0.07863354682922363,0.03981281444430351,0.003000518772751093,0.03953244164586067,0.04869844764471054,0.019000589847564697,-0.002482909243553877,-0.026700034737586975,0.0006197163020260632,0.01695171929895878,0.0001299921568715945,-0.011473681777715683,0.007214185316115618,-0.018299659714102745,0.014007813297212124,-0.02311990037560463,0.003488473827019334,-0.008993469178676605,0.02122199907898903,0.024198254570364952,0.011355062946677208,-0.02344340644776821,-0.06349346041679382,0.006453946232795715,-0.025945186614990234,-0.0024883009027689695,-0.014396020211279392,-0.020531851798295975,0.004386203363537788,-0.026419663801789284,0.0022928493563085794,-0.0075808255933225155,0.009538037702441216,0.004092351999133825,-0.0005951164057478309,0.013608822599053383,-0.0019828227814286947,-0.002198493340983987,0.012476551346480846,-0.0012286491692066193,-0.01622922159731388,-0.03256627917289734,0.013878410682082176,-0.00048525910824537277,-0.0051140920259058475,-0.01695171929895878,0.012897108681499958,0.02885674126446247,-0.020855357870459557,-0.03834625333547592,-0.03603857755661011,-0.013749008066952229,0.002135140122845769,0.0005799520295113325,-0.029546888545155525,-0.014924413524568081,-0.030452705919742584,0.003933294676244259,0.015312621369957924,0.0058500682935118675,0.019162343814969063,-0.0016242701094597578,0.018310444429516792,-1.248952503374312e-05,0.043975260108709335,-0.016293922439217567,-0.03138009086251259,-0.029266515746712685,0.02249445579946041,0.007348979823291302,-0.04740442335605621,0.008109219372272491,-0.027735253795981407,0.023961016908288002,-0.00865917932242155,-0.0055966549552977085,-0.02743331529200077,-0.02805875986814499,-0.0065294308587908745,0.00671814288944006,-0.022839529439806938,-0.03431321308016777,0.014180350117385387,0.0379580482840538,-0.03364463150501251,-0.041840121150016785,0.012314798310399055,0.006825978402048349,-0.02093084342777729,0.00037472788244485855,0.03155262768268585,0.02572951652109623,-0.013878410682082176,-0.028468534350395203,-0.013447069562971592,-0.016488026827573776,-0.060646604746580124,0.02639809623360634,-0.01723209023475647,-0.03437791392207146,-0.00028357328847050667,-0.004653095733374357,0.008707704953849316,0.0015703524695709348,-0.007408289238810539,-0.00582850119099021,-0.020542636513710022,0.011026165448129177,0.04395369067788124,0.01674683205783367,-0.007316628936678171,0.01444993820041418,-0.0028145029209554195,-0.03301918879151344,0.03651305288076401,-0.02383161522448063,-0.02639809623360634,-0.029827259480953217,-0.00996398739516735,-0.012616736814379692,-0.021049462258815765,-0.0031946224626153708,0.02620399184525013,-0.01689780130982399,-0.06474435329437256,-0.016919367015361786,-0.023098334670066833,0.0072034019976854324,-0.030625242739915848,0.03478768840432167,-0.00797442439943552,0.01751246303319931,-0.015291053801774979,-0.022753261029720306,-0.035585667937994,0.024564895778894424,0.00011617575364653021,-0.012487334199249744,0.002071786904707551,0.04542025178670883,-0.018731001764535904,0.03575820475816727,0.011117825284600258,-0.026743169873952866,-0.01521556917577982,-0.01585179753601551,0.024133553728461266,0.029266515746712685,0.0010271318024024367,0.03435634449124336,0.018677083775401115,0.020855357870459557,0.025902053341269493,-0.04727502167224884,-0.0074190725572407246,-0.030172333121299744,0.015938065946102142,-0.027562716975808144,0.018687868490815163,0.01274613942950964,-0.025751084089279175,0.004275672137737274,0.01314513012766838,0.013964679092168808,0.018008505925536156,-0.020639687776565552,-0.013986245729029179,0.0021715345792472363,0.041602883487939835,0.023141467943787575,0.0006648724083788693,-0.025362877175211906,-0.007753362413495779,-0.00966204795986414,0.01894667185842991,0.034334778785705566,-0.017728133127093315,-0.008928767405450344,0.0189790241420269,0.022278785705566406,0.011258011683821678,0.030409570783376694,-0.006464730016887188,-0.005122179631143808,0.04039512574672699,-0.03295448422431946,0.010508555918931961,0.008389591239392757,0.004661183338612318,0.012066776864230633,0.010524731129407883,0.03390343859791756,-0.006653441581875086,0.009451769292354584,0.04524771496653557,0.002025956753641367,-0.003728407435119152,-0.029935095459222794,0.004857982974499464,-0.030452705919742584,-0.021707257255911827,-0.04265966638922691,0.009796842001378536,-0.03241530805826187,-0.03301918879151344,0.0006914942641742527,-0.0037877170834690332,0.001671448117122054,-0.016434108838438988,-0.01203442644327879,0.02605302259325981,0.017124254256486893,0.0061088730581104755,-0.012713789008557796,-0.017210522666573524,-0.012853974476456642,0.009101304225623608,-0.018332011997699738,0.005521170329302549,-0.029697857797145844,-0.0008593130041845143,-0.05680766701698303,0.035434700548648834,0.04818083718419075,0.019960325211286545,-0.0023723780177533627,0.003121833549812436,0.020618120208382607,0.028511669486761093,-0.017005635425448418,0.026980407536029816,-0.023723779246211052,-0.026096157729625702,-0.006896071135997772,0.02620399184525013,-0.00048290021368302405,-0.017771266400814056,0.003919815178960562,-0.003499257378280163,0.03420537710189819,0.0057584079913794994,-0.0204132329672575,0.043177276849746704,-0.013759791851043701,-0.00582850119099021,-0.007920507341623306,0.02292579784989357,0.01808398962020874,0.004779802169650793,-0.04718875139951706,0.035715069621801376,0.008767014369368553,-0.0018925105687230825,0.009414026513695717,0.013544120825827122,0.06828135251998901,-0.0006187053513713181,-0.02084457501769066,0.039230503141880035,-0.004671967122703791,-0.009079737588763237,-0.035585667937994,0.010869803838431835,-0.02648436464369297,0.007478381972759962,-0.05715274065732956,-0.0011700136819854379,0.021944494917988777,-0.009980162605643272,-0.03161732852458954,0.004162444733083248,-0.014406803995370865,-0.0007177790976129472,-0.03981281444430351,-0.0028360700234770775,0.015743961557745934,0.015571425668895245,0.0074406396597623825,0.0056182220578193665,-0.0075376913882792,-0.025082504376769066,0.02795092575252056,-0.03345052897930145,0.007214185316115618,0.008702313527464867,0.004429337568581104,-0.027692120522260666,0.014352886006236076,0.011559950187802315,0.016078252345323563,-0.03271724656224251,0.0389285646378994,0.009888502769172192,-0.012347148731350899,-0.008405766449868679,0.009300800040364265,-0.04636920616030693,-0.026549065485596657,0.016919367015361786,-0.05111395940184593,0.01709190383553505,0.02572951652109623,0.008319498039782047,-0.019830921664834023,0.04332824796438217,0.025362877175211906,-0.03301918879151344,-0.011430548503994942,0.005348633974790573,-0.0043996828608214855,0.03155262768268585,0.05715274065732956,0.002981647616252303,0.011710920371115208,-0.014352886006236076,-0.02833913266658783,0.012832407839596272,0.07293983548879623,-0.006626483052968979,-0.012314798310399055,0.017307575792074203,-0.009570388123393059,0.013350017368793488,0.04076176509261131,0.02296893112361431,0.014277401380240917,-0.0061088730581104755,-0.029978230595588684,0.016078252345323563,0.004092351999133825,-0.036901261657476425,-0.04054609686136246,0.0011774273589253426,-0.00262309517711401,-0.02083379216492176,-0.005227318964898586,0.011710920371115208,0.040373560041189194,0.01737227663397789,0.00539985578507185,-0.023896316066384315,-0.015129300765693188,0.016638996079564095,0.012109911069273949,-0.017695782706141472,0.009338541887700558,0.06099167838692665,-0.002104137558490038,0.002100093523040414,0.014115648344159126,0.01798693835735321,-0.04283220320940018,0.010848237201571465,0.011969724670052528,0.0168546661734581,0.02192292921245098,0.014126432128250599,-0.02482369914650917,0.010389937087893486,-0.023098334670066833,0.01717817224562168,-0.007198010105639696,-0.035866040736436844,0.003383334493264556,0.02093084342777729,-0.005488819908350706,0.061207350343465805,-0.01671447977423668,0.00505478261038661,0.0024977365974336863,0.014697959646582603,0.01646645925939083,0.026937272399663925,-0.018526114523410797,0.050294410437345505,0.013242182321846485,0.036901261657476425,0.03390343859791756,0.01870943419635296,0.025880485773086548,0.013943111523985863,-0.014622475020587444,0.0013951199362054467,-0.01430975180119276,0.0379796139895916,-0.02596675418317318,0.013781358487904072,0.009775275364518166,0.006103481166064739,0.007985208183526993,0.014945981092751026,0.00431880634278059,0.010546297766268253,0.008179311640560627,0.005882418714463711,0.020909275859594345,-0.02877047471702099,0.008163136430084705,0.030970314517617226,-0.0051572262309491634,0.006394636817276478,-0.013468636199831963,0.042551834136247635,0.011408980935811996,0.01998189277946949,-0.013759791851043701,0.018332011997699738,0.007478381972759962,-0.00366640230640769,-0.014719526283442974,-0.026980407536029816,0.008864066563546658,0.02013286203145981,0.02378848008811474,-0.025147205218672752,0.05025127902626991,-0.04291847348213196,0.02810189500451088,-0.00935471709817648,-0.02672160230576992,-0.030258601531386375,-0.028037194162607193,-0.035003356635570526,0.0038793771527707577,-0.02710980921983719,0.015463590621948242,-0.0017536724917590618,-0.02136218547821045,0.020014243200421333,-0.021146513521671295,0.013716657646000385,0.0005428836448118091,-0.0050574783235788345,0.011958940885961056,-0.00041482914821244776,0.002569177420809865,-0.034291643649339676,-0.01322061475366354,-0.004844503477215767,-0.037763942033052444,0.0035396956373006105,0.015733178704977036,0.041753850877285004,0.013943111523985863,-0.01970151998102665,0.02203076332807541,0.00412739859893918,0.010567865334451199,-0.00925766583532095,0.022666992619633675,0.0031056583393365145,-0.01780361868441105,0.0016215741634368896,0.012476551346480846,-0.013608822599053383,-0.008486642502248287,-0.0029223382007330656,0.0074136811308562756,0.012066776864230633,0.0018399407854303718,-0.008777798153460026,0.030452705919742584,-0.017242873087525368,0.030258601531386375,-0.03698752820491791,0.06634031236171722,0.02572951652109623,-0.017922237515449524,0.025556979700922966,0.00579075887799263,0.01812712475657463,0.0035181285347789526,0.03483081981539726,0.038712892681360245,-0.007990600541234016,-0.0069014630280435085,-0.05158843472599983,0.03534843027591705,0.05193350836634636,0.05063948407769203,0.022516023367643356,-0.009640481323003769,0.009219923056662083,-0.007009298540651798,-0.008400374092161655,0.05197664350271225,0.009500294923782349,-0.041559748351573944,0.011026165448129177,-0.0005910725449211895,0.011171743273735046,-0.010540906339883804,0.00828175526112318,-0.0033779426012188196,0.03813058137893677,-0.016293922439217567,0.02616085857152939,0.006276017986238003,-0.029223382472991943,0.00487685389816761,0.025082504376769066,-0.032587844878435135,0.03886386379599571,0.03584447503089905,0.009365500882267952,0.0038497222121804953,-0.021998412907123566,-0.019011374562978745,-0.03127225488424301,0.01203442644327879,0.028317565098404884,0.0033860302064567804,0.02065047062933445,-0.01474109385162592,0.03248000890016556,0.04675741121172905,-0.0006823956500738859,-0.0194642823189497,0.012282447889447212,0.03327799215912819,-0.013554904609918594,0.00575301656499505,0.03817371651530266,0.030862480401992798,-0.013684307225048542,-0.03502492606639862,0.008998860605061054,0.009505687281489372,-0.0012926763156428933,-0.05301186442375183,0.0018803790444508195,-0.0225375909358263,0.011214877478778362,-0.0036017009988427162,0.02084457501769066,-0.003798500634729862,0.00965665653347969,-0.006631874479353428,-0.009333150461316109,0.01614295318722725,-0.003022085875272751,-0.018968239426612854,-0.023184603080153465,-0.005256973672658205,0.00787737313657999,0.0025085201486945152,0.0010365673806518316,-0.02842540107667446,-0.005947120022028685,-0.0021068332716822624,0.03360149636864662,-0.00644316291436553,0.00715487590059638,0.018332011997699738,-0.017296791076660156,0.031444791704416275,0.0004724536556750536,-0.01580866426229477,-0.015603776089847088,0.011430548503994942,0.03573663905262947,-0.0004936837358400226,0.019809355959296227,-0.006755885202437639,0.02782152220606804,-0.016358623281121254,-0.01884962059557438,0.022063113749027252,-0.009311582893133163,0.0006699271616525948,0.012800057418644428,0.036081712692976,-0.016822315752506256,-0.015970416367053986,-0.013662739656865597,-0.0012171915732324123,0.03498179093003273,-0.027282346040010452,0.004281063564121723,0.019712302833795547,0.005065565928816795,-0.015409672632813454,-0.01336080115288496,0.010012513026595116,-0.01652037724852562,0.010632566176354885,-0.010907546617090702,-0.011937374249100685,-0.007516124751418829,0.008195486851036549,0.0185153316706419,0.021815093234181404,-0.03841095417737961,0.02335713803768158,0.028943009674549103,0.03854035586118698,0.01790066994726658,0.0031811431981623173,0.010610999539494514,-0.012810840271413326,0.017879102379083633,-0.047533825039863586,0.011376630514860153,-0.02695883996784687,0.019302530214190483,-0.029978230595588684,0.03118598647415638,0.019669169560074806,0.014568557031452656,-0.0088964169844985,-0.0003381649439688772,0.0027794563211500645,0.0107997115701437,0.019572118297219276,0.018105557188391685,0.012552035972476006,0.010411503724753857,-0.026570633053779602,-0.003925207071006298,-0.0159057155251503,-0.0016296618850901723,-0.014191132970154285,0.0029412093572318554,-0.026225559413433075,-0.017480112612247467,0.0033132415264844894,-0.012649087235331535,0.019162343814969063,0.012422633357346058,-0.02430609054863453,0.02340027317404747,-0.028209729120135307,-0.004105831496417522,0.002765977056697011,-0.022192517295479774,-0.015398888848721981,-0.017102688550949097,0.04710248485207558,-0.0044050742872059345,0.018569249659776688,-0.021966062486171722,-5.214850534684956e-05,-0.005580479744821787,0.037483569234609604,0.06478748470544815,-0.027605852112174034,-0.006707359571009874,-0.008643004111945629,-0.013705873861908913,0.0038658976554870605,-0.02073673903942108,0.017825184389948845,0.007122525479644537,0.009300800040364265,0.009791450574994087,0.017027202993631363,0.005774583667516708,0.032156504690647125,0.007149484474211931,-0.010837453417479992,-0.008324889466166496,-0.02581578493118286,-0.0019545159302651882,-0.014967547729611397,-0.005526562221348286,0.004604569636285305,-0.007942073978483677,-0.04774949699640274,0.012897108681499958,-0.031099718064069748,-0.001965299481526017,0.006993122864514589,0.03640521690249443,0.044730108231306076,0.017534028738737106,0.03838938847184181,-0.018116340041160583,-0.027260778471827507,-0.013964679092168808,-0.04104213789105415,0.01989562436938286,-0.030862480401992798,0.02510407194495201,-0.017124254256486893,-0.005877027288079262,-0.008443508297204971,0.022429754957556725,-0.0014085994334891438,0.015927283093333244,-0.003814675845205784,-0.013608822599053383,-0.02340027317404747,0.060172129422426224,0.0010985727421939373,-0.021825876086950302,0.013274532742798328,-0.002113573020324111,-0.010481596924364567,0.00987232755869627,0.01823495887219906,0.01879570260643959,0.0032269731163978577,-0.032674115151166916,0.01072961837053299,0.016035117208957672,-0.03439947962760925,0.018051639199256897,0.004097743425518274,-0.004423945676535368,0.012853974476456642,0.006567173171788454,0.055944982916116714,-0.005027823615819216,0.0012488682987168431,0.016908584162592888,0.00286033283919096,0.006939205341041088,-0.022278785705566406,0.00435654865577817,-0.017738915979862213,0.02021913044154644,-0.02629026025533676,-0.015787096694111824,-0.037009093910455704,-0.02596675418317318,0.00828175526112318,0.012703005224466324,0.011937374249100685,-0.018159475177526474,-0.04969053342938423,-0.028684206306934357,0.016692914068698883,-0.03603857755661011,-0.013069645501673222,-0.0009368197061121464,0.0065240394324064255,-0.0026689250953495502,-0.0102012250572443,-0.02268856018781662,-0.012713789008557796,-0.03877759352326393,0.02790779061615467,-0.020801441743969917,0.0002916609519161284,-0.0022133206948637962,0.025902053341269493,-0.006971555761992931,0.01989562436938286,0.007068607956171036,-0.014546990394592285,-0.00897190161049366,0.008545951917767525,-0.00524619035422802,-0.03698752820491791,-0.006626483052968979,0.02288266271352768,0.011408980935811996,-0.021588638424873352,-0.028554802760481834,0.0012016902910545468,-0.027778388932347298,-0.024758998304605484,-0.01322061475366354,-0.03722476586699486,0.025751084089279175,-0.05434902012348175,-0.0017078424571081996,0.023723779246211052,-0.0389069989323616,0.01298337709158659,0.016304707154631615,0.05201977863907814,0.015096950344741344,-0.011732487007975578,-0.0022483672946691513,-0.021135730668902397,-0.008324889466166496,-0.00503051932901144,0.01927017793059349,-0.00012813873763661832,-0.008885633200407028,-0.038475655019283295,-0.016358623281121254,-0.00233867927454412,0.0055912635289132595,0.011333496309816837,0.019863273948431015,0.012023642659187317,0.0016903192736208439,0.011872673407196999,0.004577611107379198,0.01559299323707819,-0.007920507341623306,-0.020639687776565552,0.04593786224722862,-0.006114264950156212,0.02976255863904953,-0.01932409591972828,0.031358521431684494,0.003663706360384822,0.0035693503450602293,-0.011710920371115208,0.024478627368807793,0.00249234470538795,0.011667786166071892,0.004283759742975235,-0.01545280683785677,-0.004014171194285154,0.015743961557745934,0.012595170177519321,-0.015043032355606556,0.022365054115653038,-0.027303913608193398,-0.0230120662599802,-0.02838226594030857,-0.0032242771703749895,0.016531160101294518,0.02458646148443222,0.006141223944723606,0.020909275859594345,-0.006847545504570007,0.004990081302821636,0.004874158184975386,-0.0025840047746896744,-0.008767014369368553,0.0005068262107670307,-0.013403935357928276,-0.03090561367571354,-0.02833913266658783,-0.02877047471702099,0.0023265478666871786,-0.017544813454151154,0.028209729120135307,0.022710125893354416,0.03297605365514755,0.009721357375383377,0.004787889774888754,0.005558912642300129,0.016887016594409943,0.02486683428287506,0.005893202498555183,0.007176443003118038,0.006825978402048349,0.026311827823519707,-0.011764837428927422,-0.0067828441970050335,0.037052229046821594,0.02193371206521988,0.010168874636292458,-0.011829539202153683,0.013608822599053383,0.005205751862376928,-0.012767706997692585,-0.017868319526314735,0.003067916026338935,-0.009300800040364265,-0.0026621853467077017,-0.02277482859790325,-0.04580846056342125,0.01955055072903633,0.043457649648189545,0.030625242739915848,-0.042228326201438904,-0.011225661262869835,0.004585698712617159,0.028921443969011307,-0.02771368809044361,-0.0026635334361344576,-0.025600114837288857,0.02060733735561371,-0.009333150461316109,-0.04774949699640274,0.023745346814393997,-0.036534618586301804,-0.01476266048848629,0.05676453188061714,0.02122199907898903,0.02140531875193119,-0.03166045993566513,-0.0037796292454004288,-0.0020731347613036633,0.009705182164907455,-0.001767151989042759,-0.04229302704334259,0.0016390974633395672,-0.026851003989577293,-0.04662800952792168,0.058834973722696304,-0.04550652205944061,-0.02767055295407772,-0.010077213868498802,-0.008179311640560627,-0.006400028709322214,0.028403833508491516,0.015830229967832565,0.012325581163167953,0.0332132913172245,0.0010129783768206835,-0.021901361644268036,-0.00733280461281538,0.004388899076730013,-0.022278785705566406,0.01652037724852562,-0.006739709991961718,-0.010411503724753857,0.00041988393059000373,0.026937272399663925,0.013468636199831963,0.019755437970161438,0.009624306112527847,-0.03118598647415638,-0.024931535124778748,0.05443529039621353,0.01112860906869173,0.02596675418317318,0.004693534225225449,-0.01414799876511097,0.004510214086622,-0.02980569377541542,0.03138009086251259,-0.005480732303112745,-0.006044171750545502,0.027692120522260666,-0.00828175526112318,-0.0389285646378994,0.04947486147284508,-0.0025745693128556013,0.0031838389113545418,-0.0008660527528263628,-0.024715865030884743,-0.0014180350117385387,0.03312702104449272,-0.008783189579844475,-0.002701275749132037,-0.02311990037560463,0.006281409878283739,-0.0008903156849555671,-0.010179657489061356,0.028986144810914993,0.056548863649368286,-0.0168546661734581,-0.01267065480351448,-0.032587844878435135,-0.016488026827573776,0.018644733354449272,0.022947363555431366,4.0480383177055046e-05,0.024457059800624847,-0.00942481029778719,0.03198396787047386,0.028446968644857407,-0.0021863619331270456,0.011861889623105526,0.02292579784989357,0.0008384199463762343,0.027303913608193398,0.014342103153467178,0.0002136488037649542,-0.01312356349080801,-0.00334020028822124,0.012541252188384533,0.026807870715856552,0.0038416346069425344,-0.0045668273232877254,0.023227736353874207,-0.0006729600136168301,0.026786303147673607,-0.010260534472763538,-0.03222120553255081,0.003162271808832884,-0.021114163100719452,0.010621783323585987,0.008944942615926266,0.002895379438996315,0.002255107043311,0.02592362090945244,-0.02913711406290531,-0.0037769335322082043,0.0015784400748088956,-0.025039371103048325,0.009144438430666924,0.004000691697001457,0.017404627054929733,-0.022861097007989883,0.006324543617665768,0.028490101918578148,0.031207552179694176,0.026570633053779602,0.011355062946677208,-0.004928075708448887,0.00788276456296444,-0.009478728286921978,-0.02691570483148098,0.0006527408841066062,-0.011171743273735046,0.015398888848721981,-0.024845266714692116,0.019097642973065376,0.049345459789037704,0.014881279319524765,0.03944617509841919,-0.008928767405450344,-0.03979124873876572,-0.009505687281489372,-0.012217746116220951,0.02434922382235527,0.021631773561239243,0.0075754341669380665,0.037375736981630325,0.0027605851646512747,-0.0024357312358915806,0.025643248111009598,0.008292539045214653,0.0159057155251503,-0.023270869627594948,-0.051329631358385086,0.02700197324156761,-0.03379560261964798,0.017264440655708313,0.01812712475657463,0.005418726708739996,-0.025664815679192543,0.002643314190208912,0.013403935357928276,-0.022796394303441048,-0.02534130960702896,0.010265925899147987,-0.008578303270041943,0.011926590465009212,0.012314798310399055,0.0019113817252218723,0.016337057575583458,-0.026570633053779602]) as similarity_score
FROM shared_source.magento_product_dimension_with_text_embedding
ORDER BY similarity_score DESC
LIMIT 200
),
filtered_matches AS (
SELECT * FROM vector_matches
WHERE gender_by_product IN ('women', 'unisex') AND age_by_product = 'adult' AND (LOWER(master_color) LIKE '%tím%' OR LOWER(product_color_name) LIKE '%tím%') AND LOWER(product_name) LIKE '%liền%'
ORDER BY similarity_score DESC
LIMIT 150
)
SELECT
internal_ref_code,
MAX_BY(magento_ref_code, similarity_score) as magento_ref_code,
product_color_code,
MAX_BY(product_name, similarity_score) as product_name,
MAX_BY(master_color, similarity_score) as master_color,
MAX_BY(product_image_url_thumbnail, similarity_score) as product_image_url_thumbnail,
MAX_BY(product_web_url, similarity_score) as product_web_url,
MAX_BY(sale_price, similarity_score) as sale_price,
MAX_BY(original_price, similarity_score) as original_price,
MAX_BY(discount_amount, similarity_score) as discount_amount,
MAX_BY(discount_percent, similarity_score) as discount_percent,
MAX_BY(description_text_full, similarity_score) as description_text_full,
MAX_BY(gender_by_product, similarity_score) as gender_by_product,
MAX_BY(age_by_product, similarity_score) as age_by_product,
MAX_BY(product_line_vn, similarity_score) as product_line_vn,
MAX_BY(quantity_sold, similarity_score) as quantity_sold,
MAX(similarity_score) as max_score
FROM filtered_matches
GROUP BY product_color_code, internal_ref_code
ORDER BY max_score DESC
LIMIT 80
\ No newline at end of file
...@@ -735,7 +735,19 @@ ...@@ -735,7 +735,19 @@
<span style="font-style: normal;">🤖</span> AI is thinking... <span style="font-style: normal;">🤖</span> AI is thinking...
</div> </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"> <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..." <input type="text" id="userInput" placeholder="Type your message..."
onkeypress="handleKeyPress(event)" autocomplete="off"> onkeypress="handleKeyPress(event)" autocomplete="off">
<button onclick="sendMessage()" id="sendBtn">➤ Send</button> <button onclick="sendMessage()" id="sendBtn">➤ Send</button>
...@@ -800,7 +812,72 @@ ...@@ -800,7 +812,72 @@
let isPromptPanelOpen = false; let isPromptPanelOpen = false;
let currentPromptTab = 'system'; let currentPromptTab = 'system';
let selectedToolPrompt = ''; 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() { 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; 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 @@ ...@@ -1177,8 +1254,21 @@
const messageId = 'hist-' + (msg.id || Date.now() + Math.random()); const messageId = 'hist-' + (msg.id || Date.now() + Math.random());
if (msg.is_human) { if (msg.is_human) {
// User message: simple text // User message: show images (if any) + text
div.innerText = msg.message; 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 { } else {
// Bot message: add Widget/Raw JSON toggle // Bot message: add Widget/Raw JSON toggle
...@@ -1268,14 +1358,19 @@ ...@@ -1268,14 +1358,19 @@
sendBtn.disabled = true; sendBtn.disabled = true;
typingIndicator.style.display = 'block'; typingIndicator.style.display = 'block';
// Add user message immediately // Capture images before clearing
const imagesToSend = [...pendingImages];
// Add user message immediately (with image previews)
appendMessage({ appendMessage({
message: text, message: text,
is_human: true, is_human: true,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
id: 'pending' id: 'pending',
images: imagesToSend.length > 0 ? imagesToSend : undefined
}); });
input.value = ''; input.value = '';
clearPendingImages(); // Clear image previews after send
chatBox.scrollTop = chatBox.scrollHeight; chatBox.scrollTop = chatBox.scrollHeight;
// Save config to localStorage // Save config to localStorage
...@@ -1301,7 +1396,8 @@ ...@@ -1301,7 +1396,8 @@
headers: headers, headers: headers,
body: JSON.stringify({ body: JSON.stringify({
user_query: text, user_query: text,
device_id: deviceId device_id: deviceId,
...(imagesToSend.length > 0 && { images: imagesToSend })
}) })
}); });
...@@ -1573,7 +1669,7 @@ ...@@ -1573,7 +1669,7 @@
limitDiv.style.background = 'rgba(255, 255, 255, 0.05)'; limitDiv.style.background = 'rgba(255, 255, 255, 0.05)';
limitDiv.style.borderRadius = '6px'; limitDiv.style.borderRadius = '6px';
limitDiv.style.borderLeft = '3px solid #667eea'; 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)`; const limitText = `📊 Message Limit: ${data.limit_info.used}/${data.limit_info.limit} (Còn ${data.limit_info.remaining} tin nhắn)`;
limitDiv.innerText = limitText; limitDiv.innerText = limitText;
botMsgDiv1.appendChild(limitDiv); 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