Commit 561d7ff4 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

Optimize search performance: push HARD filters to SQL and eliminate redundant processing

parent 17f9182c
...@@ -53,3 +53,5 @@ Thumbs.db ...@@ -53,3 +53,5 @@ Thumbs.db
*.log *.log
!backend/requirements.txt !backend/requirements.txt
run.txt run.txt
backend/agent/tools/query.txt
backend/schema_dump.json
...@@ -73,7 +73,7 @@ class CANIFAGraph: ...@@ -73,7 +73,7 @@ class CANIFAGraph:
pass pass
# 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 = state.get("user_insight") or "Chưa có thông tin." user_insight_text = state.get("user_insight") or "⚠️ TRẠNG THÁI KHỞI TẠO: Chưa có User Insight từ lịch sử. Hãy bắt đầu thu thập thông tin mới (Nếu thiếu thông tin thì ghi 'Chưa rõ')."
response = await self.chain.ainvoke({ response = await self.chain.ainvoke({
"history": history, "history": history,
......
...@@ -108,6 +108,7 @@ async def fetch_products_by_skus(skus: list[str]) -> list[dict]: ...@@ -108,6 +108,7 @@ async def fetch_products_by_skus(skus: list[str]) -> list[dict]:
sql = f""" sql = f"""
SELECT SELECT
internal_ref_code, internal_ref_code,
product_color_code,
description_text_full, description_text_full,
sale_price, sale_price,
original_price, original_price,
...@@ -116,11 +117,13 @@ async def fetch_products_by_skus(skus: list[str]) -> list[dict]: ...@@ -116,11 +117,13 @@ async def fetch_products_by_skus(skus: list[str]) -> list[dict]:
product_line_en, product_line_en,
1.0 as max_score 1.0 as max_score
FROM shared_source.magento_product_dimension_with_text_embedding FROM shared_source.magento_product_dimension_with_text_embedding
WHERE internal_ref_code IN ({placeholders}) OR magento_ref_code IN ({placeholders}) WHERE internal_ref_code IN ({placeholders})
OR magento_ref_code IN ({placeholders})
OR product_color_code IN ({placeholders})
""" """
# Params: Pass SKUs twice (once for internal_ref, once for magento_ref) # Params: Pass SKUs 3 times (internal_ref, magento_ref, product_color_code)
params = skus + skus params = skus + skus + skus
try: try:
results = await db.execute_query_async(sql, params=params) results = await db.execute_query_async(sql, params=params)
......
...@@ -237,10 +237,10 @@ Anh xem thêm nhé." ...@@ -237,10 +237,10 @@ Anh xem thêm nhé."
Em vừa kiếm được 2 mẫu SIÊU XINH màu hồng cho chị ấy nè: Em vừa kiếm được 2 mẫu SIÊU XINH màu hồng cho chị ấy nè:
🌸 [6TP25S004]: Áo polo basic hồng - 299k 🌸 [6TP25S004]: Áo polo basic hồng - 299k (Còn 3 cái size S!) 🔥
→ Chất dệt kim mềm mịn, mặc cả ngày không nóng, phối quần jeans là chuẩn! → Chất dệt kim mềm mịn, mặc cả ngày không nóng, phối quần jeans là chuẩn!
🌸 [6TP25S005]: Áo polo họa tiết kẻ - 279k 🌸 [6TP25S005]: Áo polo họa tiết kẻ - 279k (Sẵn hàng đủ size) ✅
→ Form slimfit tôn dáng cực, đang SALE nóng bỏng tay luôn! → Form slimfit tôn dáng cực, đang SALE nóng bỏng tay luôn!
Anh kéo xuống xem ảnh ngay đi, chắc chắn vợ thích mê luôn! Anh kéo xuống xem ảnh ngay đi, chắc chắn vợ thích mê luôn!
...@@ -274,7 +274,31 @@ Mẫu nào bắt mắt nhất để em tư vấn size cho chị ấy nè? 😍" ...@@ -274,7 +274,31 @@ Mẫu nào bắt mắt nhất để em tư vấn size cho chị ấy nè? 😍"
``` ```
🌸 [1DS25W006]: Váy liền bé gái hoạ tiết 🌸 [1DS25W006]: Váy liền bé gái hoạ tiết
→ Giá gốc 299k, còn 149k thôi! SALE SỐC 50%! 🔥 → Giá gốc 299k, còn 149k thôi! SALE SỐC 50%! 🔥
→ Form xòe có nơ, cực kỳ đáng yêu cho bé gái. → Prices...
### 📦 QUY TẮC HIỂN THỊ TỒN KHO (BẮT BUỘC):
**LUÔN đọc thông tin tồn kho (`qty`) từ tool trả về và BÁO KHÁCH NGAY:**
1. **Còn ít (< 5 cái):** 🚨 BÁO ĐỘNG ĐỎ! (Tạo Urgency mạnh)
- "Chỉ còn **2 cái** size M thôi!"
- "Sắp hết hàng size L (còn **1 cái** duy nhất)!"
- "⚠️ Last piece! Nhanh tay kẻo lỡ!"
2. **Còn trung bình (5-10 cái):** ⚡ CẢNH BÁO NHẸ
- "Size S đang bán chạy, còn vài cái thôi."
- "Lượng tồn kho đang giảm nhanh!"
3. **Còn nhiều (> 10 cái):** ✅ SẴN HÀNG (Nhưng vẫn giục mua)
- "Sẵn hàng đủ size, nhưng chốt sớm để được ship ngay!"
- "Kho đang sẵn, order là ship liền!"
**FORMAT HIỂN THỊ TRONG RESPONSE:**
- **[SKU] Tên sp - Giá (Tình trạng kho)**
- VD: "[6TP25S004] Áo polo hồng - 299k (Còn 2 cái size M!) 🔥"
- VD: "[8TS24W001] Áo thun basic - 150k (Sẵn hàng đủ size) ✅"
**TUYỆT ĐỐI KHÔNG BỎ QUA THÔNG TIN TỒN KHO!** Khách rất cần biết còn hàng hay không.
``` ```
**VÍ DỤ VĂN PHONG ĐÚNG:** **VÍ DỤ VĂN PHONG ĐÚNG:**
...@@ -610,14 +634,21 @@ Anh có muốn em tìm thêm phụ kiện phối với váy này không? 😊" ...@@ -610,14 +634,21 @@ Anh có muốn em tìm thêm phụ kiện phối với váy này không? 😊"
### 5.1. GỌI `data_retrieval_tool` KHI: ### 5.1. GỌI `data_retrieval_tool` KHI:
**⚡ ƯU TIÊN TUYỆT ĐỐI KHÔNG CẦN HỎI LẠI:**
- **KHI KHÁCH CUNG CẤP MÃ SẢN PHẨM (SKU):**
- Ví dụ: "6DS25S012", "8TS24W009", "Cái mã này giá bao nhiêu 6VP24W001"...
- **HÀNH ĐỘNG NGAY:** Gọi `data_retrieval_tool` với query chính là mã đó.
- **TỰ ĐỘNG SUY LUẬN:** KHÔNG ĐƯỢC HỎI LẠI "Nam hay nữ?", "Màu gì?". Cứ tìm mã đó trước. Có data rồi tính.
**CÁC TRƯỜNG HỢP KHÁC:**
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì...", "Áo thun nam", "Muốn mua váy" - Khách tìm sản phẩm: "Tìm áo...", "Có màu gì...", "Áo thun nam", "Muốn mua váy"
- Khách hỏi sản phẩm cụ thể: "Mã 8TS24W001 có không?"
- Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?", "Áo cho đàn ông đi chơi" - Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?", "Áo cho đàn ông đi chơi"
- So sánh sản phẩm: "So sánh áo thun vs áo len", "Giữa X và Y nên chọn cái nào" - So sánh sản phẩm: "So sánh áo thun vs áo len", "Giữa X và Y nên chọn cái nào"
- Mua cho nhiều người: "Tư vấn 2tr cho gia đình 5 người" - Mua cho nhiều người: "Tư vấn 2tr cho gia đình 5 người"
**LƯU Ý:** Ngay cả khi khách nói kèm size (VD: "Tìm áo size M"), vẫn dùng `data_retrieval_tool` để tìm sản phẩm trước. Chỉ dùng `canifa_knowledge_search` khi khách hỏi "cách chọn size" hoặc "bảng size". **LƯU Ý:** Ngay cả khi khách nói kèm size (VD: "Tìm áo size M"), vẫn dùng `data_retrieval_tool` để tìm sản phẩm trước. Chỉ dùng `canifa_knowledge_search` khi khách hỏi "cách chọn size" hoặc "bảng size".
--- ---
### 5.2. QUY TẮC SINH QUERY (BẮT BUỘC) ### 5.2. QUY TẮC SINH QUERY (BẮT BUỘC)
...@@ -939,6 +970,13 @@ price_max = 400000 ...@@ -939,6 +970,13 @@ price_max = 400000
}} }}
``` ```
### ⚠️ QUY TẮC SỐNG CÒN (ANTI-HALLUCINATION):
1. **KHÔNG BAO GIỜ COPY VÍ DỤ MINH HỌA**: Tuyệt đối không được lấy thông tin "Nam, 1m72, 70kg" từ ví dụ để điền vào nếu khách không nói.
2. **KHÔNG BIẾT THÌ GHI "Chưa rõ"**:
- Nếu khách chưa nói chiều cao/cân nặng → Ghi "Chưa rõ".
- Nếu khách chưa nói giới tính → Ghi "Chưa rõ".
3. **CHỈ UPDATE KHI CÓ THÔNG TIN THỰC TẾ**: Chỉ ghi nhận thông tin khách thực sự cung cấp trong chat.
--- ---
### 8.2. CHI TIẾT TỪNG TẦNG ### 8.2. CHI TIẾT TỪNG TẦNG
...@@ -1374,7 +1412,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -1374,7 +1412,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
**Output (RAW JSON - KHÔNG CÓ ```json):** **Output (RAW JSON - KHÔNG CÓ ```json):**
{{ {{
"ai_response": "Gu anh xịn là em nhận ra liền rồi nha! 😆😆 Em tìm được 2 mẫu HOT lắm cho anh đây! 🔥🔥\n\n[8TS24W009] cotton basic - 200k (Sale từ 250k!) ✨🎉 TRENDING!\n- Chất cotton 100%, thấm mồ hôi cực tốt 💯\n- Form suông lịch sự, mặc ngàn lần không chán 🙌\n- Hôm nay có 5+ khách order! 📈⚡\n\n[6TN24W012] áo thun trơn - 280k (Còn 3 cái last piece!) 🚨\n- Form thoải mái CHUẨN BIT, phối quần jeans = TỐP 1 👌\n- Bán chạy tuần này, review 4.9/5 ⭐ từ khách! 💪\n- Mặc vào liền thấy style rồi! 😍\n\nAnh muốn lấy ngay hôm nay không? 🛒 Order hôm nay ship liền!", "ai_response": "Gu anh xịn là em nhận ra liền rồi nha! 😆😆 Em tìm được 2 mẫu HOT lắm cho anh đây! 🔥🔥\n\n[8TS24W009] cotton basic - 200k (Sẵn hàng đủ size) ✅ ✨🎉 TRENDING!\n- Chất cotton 100%, thấm mồ hôi cực tốt 💯\n- Form suông lịch sự, mặc ngàn lần không chán 🙌\n- Hôm nay có 5+ khách order! 📈⚡\n\n[6TN24W012] áo thun trơn - 280k (Còn 3 cái last piece!) 🚨\n- Form thoải mái CHUẨN BIT, phối quần jeans = TỐP 1 👌\n- Bán chạy tuần này, review 4.9/5 ⭐ từ khách! 💪\n- Mặc vào liền thấy style rồi! 😍\n\nAnh muốn lấy ngay hôm nay không? 🛒 Order hôm nay ship liền!",
"product_ids": ["8TS24W009", "6TN24W012"], "product_ids": ["8TS24W009", "6TN24W012"],
"user_insight": {{ "user_insight": {{
"USER": "Nam, Adult (Tìm áo thun nam giá rẻ).", "USER": "Nam, Adult (Tìm áo thun nam giá rẻ).",
...@@ -1576,7 +1614,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -1576,7 +1614,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
**Output (RAW JSON - KHÔNG CÓ ```json):** **Output (RAW JSON - KHÔNG CÓ ```json):**
{{ {{
"ai_response": "Dạ mình hiểu, để mình tìm mẫu váy đen với giá nhẹ nhàng hơn cho vợ bạn nhé! 💰\n\nMình vừa check kho có mấy mẫu này:\n\n[6VP24W010]: Váy suông basic - 350k (đang sale từ 420k!) ✨\n- Chất cotton mềm, thoáng mát\n- Form basic hiện đại, không già chút nào\n\n[6VP24W012]: Váy cổ V trẻ trung - 380k\n- Kiểu dáng trẻ trung, thanh lịch\n- Mặc đi làm hay đi chơi đều ok\n\nGiá này ok hơn chưa bạn? Muốn xem kỹ mẫu nào để em tư vấn size?", "ai_response": "Dạ mình hiểu, để mình tìm mẫu váy đen với giá nhẹ nhàng hơn cho vợ bạn nhé! 💰\n\nMình vừa check kho có mấy mẫu này:\n\n[6VP24W010]: Váy suông basic - 350k (đang sale từ 420k!) (Sẵn hàng! ✅) ✨\n- Chất cotton mềm, thoáng mát\n- Form basic hiện đại, không già chút nào\n\n[6VP24W012]: Váy cổ V trẻ trung - 380k (Còn 4 cái!) ⚡\n- Kiểu dáng trẻ trung, thanh lịch\n- Mặc đi làm hay đi chơi đều ok\n\nGiá này ok hơn chưa bạn? Muốn xem kỹ mẫu nào để em tư vấn size?",
"product_ids": ["6VP24W010", "6VP24W012"], "product_ids": ["6VP24W010", "6VP24W012"],
"user_insight": {{ "user_insight": {{
"USER": "Nam, Adult, có vợ.", "USER": "Nam, Adult, có vợ.",
......
Công cụ KIỂM TRA TỒN KHO sản phẩm CANIFA theo mã sản phẩm.
KHI NÀO GỌI TOOL NÀY:
- Khách hỏi "còn hàng không?", "còn size không?", "check tồn kho"
- Khách hỏi về MÃ SẢN PHẨM CỤ THỂ kèm từ khóa tồn kho (vd: "8TS24W001 còn size L không?")
- Khách muốn biết số lượng tồn kho của một hoặc nhiều mã sản phẩm
KHÔNG GỌI TOOL NÀY:
- Khách tìm kiếm sản phẩm theo mô tả (dùng data_retrieval_tool thay thế)
- Khách hỏi giá, thông tin sản phẩm (dùng data_retrieval_tool)
QUY TẮC CỰC QUAN TRỌNG KHI GỌI TOOL:
- Khi đã quyết định gọi tool, TUYỆT ĐỐI KHÔNG sinh ai_response trước.
- Chỉ tạo tool_call với đúng tham số, KHÔNG trả lời người dùng trong cùng message đó.
- Sau khi tool trả kết quả mới được sinh ai_response.
----- VÍ DỤ CHI TIẾT -----
CASE 1: KIỂM TRA TỒN KHO MÃ CỤ THỂ
User: "6TE25C019-SK010 mã này còn hàng không?"
-> Gọi check_is_stock với:
- skus: "6TE25C019-SK010"
CASE 2: KIỂM TRA NHIỀU MÃ
User: "Check tồn kho 2 mã: 8TS24W001 và 6ST25W005"
-> Gọi check_is_stock với:
- skus: "8TS24W001,6ST25W005"
CASE 3: KIỂM TRA MÃ KÈM SIZE
User: "Mã 6ST25W005-SE091 còn size M và L không?"
-> Gọi check_is_stock với:
- skus: "6ST25W005-SE091"
- sizes: "M,L"
CASE 4: KIỂM TRA MÃ BASE (TỰ EXPAND)
User: "6ST25W005 còn màu nào và size nào?"
-> Gọi check_is_stock với:
- skus: "6ST25W005"
(Tool sẽ tự động expand ra tất cả các biến thể từ DB)
CÁCH ĐỌC KẾT QUẢ:
- stock_responses: Danh sách tồn kho từng SKU
- is_in_stock: true/false - còn hàng hay không
- qty: số lượng tồn kho
- Nếu hết hàng -> Gợi ý size/màu khác còn hàng
...@@ -42,6 +42,24 @@ User: "Mua váy cho vợ/ bà xã/ người yêu" ...@@ -42,6 +42,24 @@ User: "Mua váy cho vợ/ bà xã/ người yêu"
- query: "product_name: Váy liền thân nữ/ Đầm nữ (tránh từ khóa 'bé'). gender_by_product: female. age_by_product: adult..." - query: "product_name: Váy liền thân nữ/ Đầm nữ (tránh từ khóa 'bé'). gender_by_product: female. age_by_product: adult..."
(Lưu ý: Phải set age_by_product='adult' hoặc tìm tên sản phẩm có chữ 'nữ' thay vì 'bé gái') (Lưu ý: Phải set age_by_product='adult' hoặc tìm tên sản phẩm có chữ 'nữ' thay vì 'bé gái')
Lưu ý các trường khác: ----- HƯỚNG DẪN ĐIỀN CÁC TRƯỜNG THÔNG TIN (FIELD DEFINITIONS) -----
- 'magento_ref_code': chỉ dùng khi khách hỏi mã sản phẩm/SKU cụ thể (vd: 8TS24W001).
- 'price_min' / 'price_max': dùng khi khách nói về khoảng giá (vd: dưới 500k, từ 200k đến 400k). 1. **magento_ref_code** (QUAN TRỌNG NHẤT):
- Chỉ điền khi khách cung cấp MÃ SẢN PHẨM cụ thể (VD: 6KS25S005, 8TS24W001).
- **LƯU Ý:** Nếu đã có `magento_ref_code`, hãy để `description` là hoặc mô tả ngắn gọn (VD: "Áo thun"), **TUYỆT ĐỐI KHÔNG** chèn câu "Mã sản phẩm..." vào `description` vì sẽ làm nhiễu kết quả tìm kiếm.
2. **description**:
- Dùng cho tìm kiếm ngữ nghĩa (Semantic Search).
- Mô tả sản phẩm cần tìm: tên, màu sắc, kiểu dáng, chất liệu, tính năng...
- Cấu trúc tốt: `product_name: [Tên]. master_color: [Màu]. style: [Style]...`
3. **price_min** / **price_max**:
- Chỉ điền khi khách nói về khoảng giá (VD: "dưới 500k", "từ 200k đến 1 triệu").
- Đơn vị: VNĐ (Ex: 500000).
4. **gender_by_product** / **age_by_product**:
- Bắt buộc suy luận từ đối tượng (Nam/Nữ/Bé trai/Bé gái).
- Giúp lọc chính xác sản phẩm (tránh tìm váy bé gái cho người lớn).
5. **Các trường khác (style, season, material...)**:
- Điền nếu khách có nhắc đến hoặc có thể suy luận logic từ ngữ cảnh.
...@@ -449,8 +449,20 @@ def format_product_results(products: list[dict]) -> list[dict]: ...@@ -449,8 +449,20 @@ def format_product_results(products: list[dict]) -> list[dict]:
grouped: dict[str, dict] = {} # {product_id: {product_info + variants}} grouped: dict[str, dict] = {} # {product_id: {product_info + variants}}
for p in products: for p in products:
desc_full = p.get("description_text_full", "") # Use direct columns from database if available, else fallback to regex (backward compatibility)
parsed = parse_description_text(desc_full) if "product_name" in p and p["product_name"]:
name = p["product_name"]
color_name = p.get("master_color", "")
thumb_url = p.get("product_image_url_thumbnail", "")
web_url = p.get("product_web_url", "")
else:
# Fallback to regex parsing only if columns are missing
desc_full = p.get("description_text_full", "")
parsed = parse_description_text(desc_full)
name = parsed.get("product_name", "")
color_name = parsed.get("master_color", "")
thumb_url = parsed.get("product_image_url_thumbnail", "")
web_url = parsed.get("product_web_url", "")
original_price = p.get("original_price") or 0 original_price = p.get("original_price") or 0
sale_price = p.get("sale_price") or 0 sale_price = p.get("sale_price") or 0
...@@ -459,26 +471,25 @@ def format_product_results(products: list[dict]) -> list[dict]: ...@@ -459,26 +471,25 @@ def format_product_results(products: list[dict]) -> list[dict]:
if not sku: if not sku:
continue continue
# Extract product_id từ SKU (6TW25W005-SK010 → 6TW25W005) # Extract product_id using Color Code to PREVENT grouping different colors
product_id = p.get("magento_ref_code") or sku.split("-")[0]
color_code = p.get("product_color_code", "") color_code = p.get("product_color_code", "")
color_name = parsed.get("master_color", "") product_id = color_code if color_code else (p.get("magento_ref_code") or sku)
# Tạo product entry nếu chưa có # Create product entry if not exists
if product_id not in grouped: if product_id not in grouped:
grouped[product_id] = {"product_id": product_id, "name": parsed.get("product_name", ""), "variants": []} grouped[product_id] = {"product_id": product_id, "name": name, "variants": []}
# Thêm variant (màu sắc + giá) # Add variant (color + price)
variant_label = f"{color_code} ({color_name})" if color_name else color_code variant_label = f"{color_code} ({color_name})" if color_name else color_code
grouped[product_id]["variants"].append( grouped[product_id]["variants"].append(
{ {
"sku": sku, "sku": sku,
"color_code": color_code, # Added for dedup logic "color_code": color_code,
"color": variant_label, "color": variant_label,
"price": int(original_price), "price": int(original_price),
"sale_price": int(sale_price), "sale_price": int(sale_price),
"url": parsed.get("product_web_url", ""), "url": web_url,
"thumbnail_image_url": parsed.get("product_image_url_thumbnail", ""), "thumbnail_image_url": thumb_url,
} }
) )
......
...@@ -199,81 +199,36 @@ async def _execute_single_search( ...@@ -199,81 +199,36 @@ async def _execute_single_search(
item.master_color, item.master_color,
) )
# ====== LAYER 1: HARD FILTERS (No fallback) ====== # ====== LAYER 1: HARD FILTERS (PUSHED TO SQL) ======
# Filter by PRODUCT_NAME (HARD) - must match product type # No longer needed in Python as they are in the StarRocks WHERE clause.
if item.product_name and products: # This saves significant time.
before = len(products)
products = filter_by_product_name(products, item.product_name)
logger.warning(
"📦 Product name filter (HARD): %s → %d→%d products", item.product_name, before, len(products)
)
# Filter by GENDER (HARD)
if item.gender_by_product and products:
before = len(products)
products = filter_by_gender(products, item.gender_by_product)
logger.warning(
"👤 Gender filter (HARD): %s → %d→%d products", item.gender_by_product, before, len(products)
)
# Filter by AGE (HARD)
if item.age_by_product and products:
before = len(products)
products = filter_by_age(products, item.age_by_product)
logger.warning("🎂 Age filter (HARD): %s → %d→%d products", item.age_by_product, before, len(products))
# ====== LAYER 2: SOFT FILTERS (With priority-based fallback) ======
# Only apply if we still have products from HARD filters
# ====== LAYER 2: SOFT FILTERS (Still in Python for priority fallback) ======
# 1. COLOR (with automatic fallback) # 1. COLOR (with automatic fallback)
if item.master_color and products: if item.master_color and products:
before_count = len(products) before_count = len(products)
products, info = filter_with_priority(products, item.master_color, "master_color", COLOR_MAP, "Màu") products, info = filter_with_priority(products, item.master_color, "master_color", COLOR_MAP, "Màu")
# Store fallback info for Agent's response
if info.get("fallback_used") or info.get("matched_value"): if info.get("fallback_used") or info.get("matched_value"):
all_filter_info["color"] = info all_filter_info["color"] = info
if info.get("fallback_used"): if info.get("fallback_used"):
logger.warning( logger.warning("🎨 COLOR FALLBACK: Requested '%s' → Found '%s'", info.get("requested_value"), info.get("matched_value"))
"🎨 COLOR FALLBACK: Requested '%s' → Found '%s' (%d products)",
info.get("requested_value"),
info.get("matched_value"),
len(products),
)
else:
logger.info(
"🎨 Color filter: Matched '%s' exactly (%d products)", info.get("matched_value"), len(products)
)
else: else:
logger.warning("🎨 Color filter: NO MATCH - %d → %d products", before_count, len(products)) logger.warning("🎨 Color filter: NO MATCH - %d → %d products", before_count, len(products))
# === DISABLED FILTERS (chỉ giữ name, gender, age, color) === # Combine filter info
# 2. SLEEVE - DISABLED
# 3. STYLE - DISABLED
# 4. FITTING - DISABLED
# 5. NECKLINE - DISABLED
# 6. MATERIAL - DISABLED
# 7. SEASON - DISABLED
# Combine filter info - STRUCTURE RÕRANƠ CHO AGENT
filter_info = { filter_info = {
"fallback_used": any(info.get("fallback_used") for info in all_filter_info.values()), "fallback_used": any(info.get("fallback_used") for info in all_filter_info.values()),
"filters_applied": all_filter_info, # ← Chi tiết từng filter (fallback hay không) "filters_applied": all_filter_info,
} }
# Build recommendation message for Agent # Build recommendation message for Agent
# Agent sẽ dùng cái này để báo với khách hàng
fallback_messages = [info.get("message") for info in all_filter_info.values() if info.get("message")] fallback_messages = [info.get("message") for info in all_filter_info.values() if info.get("message")]
if fallback_messages: if fallback_messages:
filter_info["recommendation_message"] = " ".join(fallback_messages) # ← Báo khách cụ thể là có fallback filter_info["recommendation_message"] = " ".join(fallback_messages)
# Log summary chi tiết # Log summary
if original_count != len(products): if original_count != len(products):
logger.info( logger.info("📊 Post-filter summary: %d → %d products.", original_count, len(products))
"📊 Post-filter summary: %d → %d products. Fallback used: %s",
original_count,
len(products),
filter_info.get("fallback_used"),
)
return format_product_results(products), filter_info return format_product_results(products), filter_info
except Exception as e: except Exception as e:
...@@ -308,29 +263,43 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str: ...@@ -308,29 +263,43 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
if filter_info: if filter_info:
all_filter_infos.append(filter_info) all_filter_infos.append(filter_info)
# ============================================================
# OPTIMIZATION: Limit to top 8 products for stock check
# Reduce latency by avoiding massive variant expansion
# ============================================================
MAX_PRODUCTS_FOR_STOCK = 10
if len(combined_results) > MAX_PRODUCTS_FOR_STOCK:
logger.info(
"⚡ Optimization: Truncating results %d -> %d for stock check",
len(combined_results),
MAX_PRODUCTS_FOR_STOCK,
)
combined_results = combined_results[:MAX_PRODUCTS_FOR_STOCK]
# ============================================================ # ============================================================
# STOCK ENRICHMENT: Fetch stock info for all products # STOCK ENRICHMENT: Fetch stock info for all products
# ============================================================ # ============================================================
skus_to_check = [] skus_to_check = []
skus_to_check = []
for product in combined_results: # helper to extract skus
# Handle Flat Result (has 'sku') def _extract_skus(prod):
if "sku" in product: # Ưu tiên lấy danh sách SKU con (nếu là Grouped Product)
skus_to_check.append(product["sku"]) if "all_skus" in prod:
return prod["all_skus"]
# Handle Grouped Result (has 'all_skus') # Nếu là sản phẩm lẻ, lấy SKU (chính là Product Color Code)
elif "all_skus" in product: if "sku" in prod:
skus_to_check.extend(product["all_skus"]) return [prod["sku"]]
# Fallback for raw results (legacy or defensive) return []
else:
sku = product.get("product_color_code") or product.get("magento_ref_code") for product in combined_results:
if sku: skus_to_check.extend(_extract_skus(product))
skus_to_check.append(sku)
stock_map = {} stock_map = {}
if skus_to_check: if skus_to_check:
logger.info(f"🔍 Checking stock for {len(skus_to_check)} SKUs: {skus_to_check[:5]}...") logger.info(f"🔍 Checking stock for {len(skus_to_check)} SKUs")
try: try:
stock_map = await fetch_stock_for_skus(skus_to_check) stock_map = await fetch_stock_for_skus(skus_to_check)
logger.info(f"📦 [STOCK] Enriched {len(stock_map)} products with stock info") logger.info(f"📦 [STOCK] Enriched {len(stock_map)} products with stock info")
...@@ -339,33 +308,20 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str: ...@@ -339,33 +308,20 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
# Merge stock info into each product # Merge stock info into each product
for product in combined_results: for product in combined_results:
# Handle Flat Result product_skus = _extract_skus(product)
if "sku" in product:
sku = product["sku"]
if sku in stock_map:
product["stock_info"] = stock_map[sku]
# Handle Grouped Result # If single SKU
elif "all_skus" in product: if len(product_skus) == 1:
# Aggregate stock for all variants s = product_skus[0]
# For brevity, we can store a map or a summary. if s in stock_map:
# Let's store a map of {sku: stock_info} product["stock_info"] = stock_map[s]
group_stock = {}
has_stock = False # If multiple SKUs (Grouped)
for s in product["all_skus"]: elif len(product_skus) > 1:
if s in stock_map: group_stock = {s: stock_map[s] for s in product_skus if s in stock_map}
group_stock[s] = stock_map[s] if group_stock:
has_stock = True
if has_stock:
product["stock_info"] = group_stock product["stock_info"] = group_stock
# Fallback logic
else:
sku = product.get("product_color_code") or product.get("magento_ref_code")
if sku and sku in stock_map:
product["stock_info"] = stock_map[sku]
# Aggregate filter info from first result for simplicity in response # Aggregate filter info from first result for simplicity in response
final_info = all_filter_infos[0] if all_filter_infos else {} final_info = all_filter_infos[0] if all_filter_infos else {}
......
import logging import logging
import time import time
import os
from common.embedding_service import create_embedding_async from common.embedding_service import create_embedding_async
...@@ -24,22 +25,34 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]: ...@@ -24,22 +25,34 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]:
"""Xây dựng điều kiện lọc từ metadata (Parameterized).""" """Xây dựng điều kiện lọc từ metadata (Parameterized)."""
clauses = [] clauses = []
# 1. Exact Match # 1. Exact Match with Mapping
exact_fields = [ # Chúng ta sử dụng Mapping để chuyển từ Tiếng Việt (LLM) sang Tiếng Anh (DB Column)
("gender_by_product", "gender_by_product"), from agent.tools.data_retrieval_filter import GENDER_MAP, AGE_MAP
("age_by_product", "age_by_product"),
("form_neckline", "form_neckline"), # Gender Mapping
] gender_val = getattr(params, "gender_by_product", None)
for param_name, col_name in exact_fields: if gender_val:
val = getattr(params, param_name, None) gender_key = gender_val.lower().strip()
if val: acceptable_genders = GENDER_MAP.get(gender_key, [gender_key])
clauses.append(f"{col_name} = %s") placeholders = ", ".join(["%s"] * len(acceptable_genders))
sql_params.append(val) clauses.append(f"gender_by_product IN ({placeholders})")
sql_params.extend(acceptable_genders)
# 2. Partial Match (LIKE)
# Age Mapping
age_val = getattr(params, "age_by_product", None)
if age_val:
age_key = age_val.lower().strip()
acceptable_ages = AGE_MAP.get(age_key, [age_key])
placeholders = ", ".join(["%s"] * len(acceptable_ages))
clauses.append(f"age_by_product IN ({placeholders})")
sql_params.extend(acceptable_ages)
# 3. Partial Match (LIKE)
partial_fields = [ partial_fields = [
("season", "season"), ("season", "season"),
("material_group", "material_group"), ("material_group", "material_group"),
# Bỏ product_name ở đây vì Vector Search xử lý loại sản phẩm (Váy/Đầm) rất tốt.
# Ép cứng LIKE %Váy% có thể làm mất kết quả "Đầm".
("product_line_vn", "product_line_vn"), ("product_line_vn", "product_line_vn"),
("style", "style"), ("style", "style"),
("fitting", "fitting"), ("fitting", "fitting"),
...@@ -101,6 +114,15 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -101,6 +114,15 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
FROM shared_source.magento_product_dimension_with_text_embedding FROM shared_source.magento_product_dimension_with_text_embedding
WHERE internal_ref_code = %s OR magento_ref_code = %s WHERE internal_ref_code = %s OR magento_ref_code = %s
""" """
# Write to query.txt for debugging
try:
query_log_path = os.path.join(os.path.dirname(__file__), "query.txt")
with open(query_log_path, "w", encoding="utf-8") as f:
f.write(f"-- [CODE SEARCH]\n-- Params: {magento_code}\n{sql}")
except Exception as e:
logger.error(f"Error writing to query.txt: {e}")
return sql, [magento_code, magento_code] return sql, [magento_code, magento_code]
# ============================================================ # ============================================================
...@@ -121,48 +143,78 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -121,48 +143,78 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
# Vector params # Vector params
v_str = "[" + ",".join(str(v) for v in query_vector) + "]" v_str = "[" + ",".join(str(v) for v in query_vector) + "]"
# Collect Params # Collect All Filters
price_params: list = [] sql_params: list = []
price_clauses = _get_price_clauses(params, price_params)
# 1. Price
price_clauses = _get_price_clauses(params, sql_params)
# 2. Metadata (Gender, Age, etc.)
metadata_clauses = _get_metadata_clauses(params, sql_params)
# 3. Special (Color, etc.)
# Note: We keep Color as Soft filter in Python normally,
# but pushing to SQL helps performance if we have 1000s of matches.
# However, the current requirement is < 1ms on filter, so SQL is best.
special_clauses = _get_special_clauses(params, sql_params)
all_clauses = price_clauses + metadata_clauses + special_clauses
where_filter = "" where_filter = ""
if price_clauses: if all_clauses:
where_filter = " AND " + " AND ".join(price_clauses) where_filter = " AND " + " AND ".join(all_clauses)
logger.info(f"💰 [PRICE FILTER] Applied: {where_filter}") logger.info(f"🎯 [SQL FILTER] Applied {len(all_clauses)} clauses")
# Build SQL # Build SQL - Selecting columns directly to AVOID regex parsing later
# NOTE: Vector v_str is safe (generated from floats) so f-string is OK here. # NOTE: Vector v_str is safe (generated from floats) so f-string is OK here.
# Using %s for vector list string might cause StarRocks to treat it as string literal '[...]' instead of array.
sql = f""" sql = f"""
WITH top_matches AS ( WITH top_matches AS (
SELECT /*+ SET_VAR(ann_params='{{"ef_search":128}}') */ SELECT /*+ SET_VAR(ann_params='{{"ef_search":128}}') */
internal_ref_code, internal_ref_code,
magento_ref_code, magento_ref_code,
product_color_code, product_color_code,
description_text_full, product_name,
master_color,
product_image_url_thumbnail,
product_web_url,
sale_price, sale_price,
original_price, original_price,
discount_amount, discount_amount,
age_by_product,
gender_by_product,
product_line_vn,
product_line_en,
description_text_full,
approx_cosine_similarity(vector, {v_str}) as similarity_score approx_cosine_similarity(vector, {v_str}) as similarity_score
FROM shared_source.magento_product_dimension_with_text_embedding FROM shared_source.magento_product_dimension_with_text_embedding
WHERE 1=1 {where_filter}
ORDER BY similarity_score DESC ORDER BY similarity_score DESC
LIMIT 200 LIMIT 100
) )
SELECT SELECT
internal_ref_code, internal_ref_code,
MAX_BY(magento_ref_code, similarity_score) as magento_ref_code, MAX_BY(magento_ref_code, similarity_score) as magento_ref_code,
MAX_BY(product_color_code, similarity_score) as product_color_code, product_color_code,
MAX_BY(description_text_full, similarity_score) as description_text_full, 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(sale_price, similarity_score) as sale_price,
MAX_BY(original_price, similarity_score) as original_price, MAX_BY(original_price, similarity_score) as original_price,
MAX_BY(discount_amount, similarity_score) as discount_amount, MAX_BY(discount_amount, similarity_score) as discount_amount,
MAX_BY(description_text_full, similarity_score) as description_text_full,
MAX(similarity_score) as max_score MAX(similarity_score) as max_score
FROM top_matches FROM top_matches
WHERE 1=1 {where_filter} GROUP BY product_color_code, internal_ref_code
GROUP BY internal_ref_code
ORDER BY max_score DESC ORDER BY max_score DESC
LIMIT 70 LIMIT 50
""" """
# Return sql and params (params only contains filter values now, not the vector) # Write to query.txt for debugging
return sql, price_params try:
query_log_path = os.path.join(os.path.dirname(__file__), "query.txt")
with open(query_log_path, "w", encoding="utf-8") as f:
f.write(f"-- [HYDE SEARCH]\n-- Params: {sql_params}\n{sql}")
except Exception as e:
logger.error(f"Error writing to query.txt: {e}")
return sql, sql_params
import asyncio
import logging import logging
from typing import Any from typing import Any
...@@ -161,10 +162,23 @@ async def check_stock(req: StockExpandRequest): ...@@ -161,10 +162,23 @@ async def check_stock(req: StockExpandRequest):
try: try:
stock_responses: list[dict[str, Any]] = [] stock_responses: list[dict[str, Any]] = []
async with httpx.AsyncClient(timeout=req.timeout_sec) as client: async with httpx.AsyncClient(timeout=req.timeout_sec) as client:
tasks = []
for chunk in _chunked(ordered_skus, req.chunk_size): for chunk in _chunked(ordered_skus, req.chunk_size):
resp = await client.get(STOCK_API_URL, params={"skus": ",".join(chunk)}) tasks.append(client.get(STOCK_API_URL, params={"skus": ",".join(chunk)}))
resp.raise_for_status()
stock_responses.append(resp.json()) # Execute all chunks in parallel
responses = await asyncio.gather(*tasks, return_exceptions=True)
for i, resp in enumerate(responses):
if isinstance(resp, Exception):
logger.error(f"❌ Error fetching chunk {i}: {resp}")
continue
try:
resp.raise_for_status()
stock_responses.append(resp.json())
except Exception as e:
logger.error(f"❌ Error processing chunk {i}: {e}")
response_payload["stock_responses"] = stock_responses response_payload["stock_responses"] = stock_responses
return response_payload return response_payload
except httpx.RequestError as exc: except httpx.RequestError as exc:
......
...@@ -17,4 +17,6 @@ sudo docker compose -f docker-compose.prod.yml up -d --build ...@@ -17,4 +17,6 @@ sudo docker compose -f docker-compose.prod.yml up -d --build
Get-NetTCPConnection -LocalPort 5000 | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force } Get-NetTCPConnection -LocalPort 5000 | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
taskkill /F /IM python.exe taskkill /F /IM python.exe
\ No newline at end of file
netstat -ano | findstr :5000 | ForEach-Object { $_.Split()[-1] } | Sort-Object -Unique | ForEach-Object { taskkill /F /PID $_ }
\ No newline at end of file
import asyncio
import json
import os
import sys
# Add parent directory to path to import common
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from common.starrocks_connection import get_db_connection
async def main():
db = get_db_connection()
try:
# 1. Describe table
schema = await db.execute_query_async("DESCRIBE shared_source.magento_product_dimension_with_text_embedding")
# 2. Get 1 sample row to see actual values
sample = await db.execute_query_async("SELECT * FROM shared_source.magento_product_dimension_with_text_embedding LIMIT 1")
output = {
"schema": schema,
"sample": sample[0] if sample else None
}
# Remove vector from output to keep it clean
if output["sample"] and "vector" in output["sample"]:
del output["sample"]["vector"]
with open("schema_dump.json", "w", encoding="utf-8") as f:
json.dump(output, f, ensure_ascii=False, indent=2)
print("Schema dumped to schema_dump.json")
except Exception as e:
print(f"Error: {e}")
if __name__ == '__main__':
asyncio.run(main())
import asyncio
import json
import os
import sys
import time
# Add parent directory to path to import common and agent
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from agent.tools.data_retrieval_tool import data_retrieval_tool, SearchItem
async def verify_optimization():
print("🚀 Verifying Performance Optimization...")
search_1 = {
"description": "váy tiểu thư sang chảnh",
"product_name": "Váy",
"gender_by_product": "Nữ",
"age_by_product": "Người lớn",
"magento_ref_code": None,
"price_min": None,
"price_max": None,
"master_color": None,
"form_sleeve": None,
"style": None,
"fitting": None,
"form_neckline": None,
"material_group": None,
"season": None,
"product_line_vn": None
}
search_2 = {
"description": "áo thun nam basic",
"product_name": "Áo thun",
"gender_by_product": "Nam",
"age_by_product": "Người lớn",
"master_color": "Đen",
"magento_ref_code": None,
"price_min": None,
"price_max": None,
"form_sleeve": None,
"style": None,
"fitting": None,
"form_neckline": None,
"material_group": None,
"season": None,
"product_line_vn": None
}
test_searches = [
SearchItem.model_validate(search_1),
SearchItem.model_validate(search_2)
]
start_total = time.time()
result_str = await data_retrieval_tool.ainvoke({"searches": test_searches})
end_total = time.time()
print(f"\nRESULTS FOR SEARCHES:")
print(f"Total Tool Execution Time: {(end_total - start_total) * 1000:.2f}ms")
# Check query.txt
query_txt_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "agent", "tools", "query.txt")
if os.path.exists(query_txt_path):
with open(query_txt_path, "r", encoding="utf-8") as f:
query_content = f.read()
print("\nLAST SQL QUERY (First 5 lines):")
print("\n".join(query_content.splitlines()[:5]))
else:
print("\n⚠️ query.txt not found!")
if __name__ == "__main__":
asyncio.run(verify_optimization())
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