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
*.log
!backend/requirements.txt
run.txt
backend/agent/tools/query.txt
backend/schema_dump.json
......@@ -73,7 +73,7 @@ class CANIFAGraph:
pass
# Invoke chain with user_query, history, and messages
# 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({
"history": history,
......
......@@ -108,6 +108,7 @@ async def fetch_products_by_skus(skus: list[str]) -> list[dict]:
sql = f"""
SELECT
internal_ref_code,
product_color_code,
description_text_full,
sale_price,
original_price,
......@@ -116,11 +117,13 @@ async def fetch_products_by_skus(skus: list[str]) -> list[dict]:
product_line_en,
1.0 as max_score
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 = skus + skus
# Params: Pass SKUs 3 times (internal_ref, magento_ref, product_color_code)
params = skus + skus + skus
try:
results = await db.execute_query_async(sql, params=params)
......
......@@ -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è:
🌸 [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!
🌸 [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!
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è? 😍"
```
🌸 [1DS25W006]: Váy liền bé gái hoạ tiết
→ 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:**
......@@ -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:
**⚡ Ư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 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"
- 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"
**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)
......@@ -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
......@@ -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):**
{{
"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"],
"user_insight": {{
"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
**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"],
"user_insight": {{
"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"
- 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 ý các trường khác:
- '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).
----- HƯỚNG DẪN ĐIỀN CÁC TRƯỜNG THÔNG TIN (FIELD DEFINITIONS) -----
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]:
grouped: dict[str, dict] = {} # {product_id: {product_info + variants}}
for p in products:
desc_full = p.get("description_text_full", "")
parsed = parse_description_text(desc_full)
# Use direct columns from database if available, else fallback to regex (backward compatibility)
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
sale_price = p.get("sale_price") or 0
......@@ -459,26 +471,25 @@ def format_product_results(products: list[dict]) -> list[dict]:
if not sku:
continue
# Extract product_id từ SKU (6TW25W005-SK010 → 6TW25W005)
product_id = p.get("magento_ref_code") or sku.split("-")[0]
# Extract product_id using Color Code to PREVENT grouping different colors
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:
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
grouped[product_id]["variants"].append(
{
"sku": sku,
"color_code": color_code, # Added for dedup logic
"color_code": color_code,
"color": variant_label,
"price": int(original_price),
"sale_price": int(sale_price),
"url": parsed.get("product_web_url", ""),
"thumbnail_image_url": parsed.get("product_image_url_thumbnail", ""),
"url": web_url,
"thumbnail_image_url": thumb_url,
}
)
......
......@@ -199,81 +199,36 @@ async def _execute_single_search(
item.master_color,
)
# ====== LAYER 1: HARD FILTERS (No fallback) ======
# Filter by PRODUCT_NAME (HARD) - must match product type
if item.product_name and products:
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 1: HARD FILTERS (PUSHED TO SQL) ======
# No longer needed in Python as they are in the StarRocks WHERE clause.
# This saves significant time.
# ====== LAYER 2: SOFT FILTERS (Still in Python for priority fallback) ======
# 1. COLOR (with automatic fallback)
if item.master_color and products:
before_count = len(products)
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"):
all_filter_info["color"] = info
if info.get("fallback_used"):
logger.warning(
"🎨 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)
)
logger.warning("🎨 COLOR FALLBACK: Requested '%s' → Found '%s'", info.get("requested_value"), info.get("matched_value"))
else:
logger.warning("🎨 Color filter: NO MATCH - %d → %d products", before_count, len(products))
# === DISABLED FILTERS (chỉ giữ name, gender, age, color) ===
# 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
# Combine filter info
filter_info = {
"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
# 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")]
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):
logger.info(
"📊 Post-filter summary: %d → %d products. Fallback used: %s",
original_count,
len(products),
filter_info.get("fallback_used"),
)
logger.info("📊 Post-filter summary: %d → %d products.", original_count, len(products))
return format_product_results(products), filter_info
except Exception as e:
......@@ -308,29 +263,43 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
if 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
# ============================================================
skus_to_check = []
skus_to_check = []
for product in combined_results:
# Handle Flat Result (has 'sku')
if "sku" in product:
skus_to_check.append(product["sku"])
# helper to extract skus
def _extract_skus(prod):
# Ưu tiên lấy danh sách SKU con (nếu là Grouped Product)
if "all_skus" in prod:
return prod["all_skus"]
# Handle Grouped Result (has 'all_skus')
elif "all_skus" in product:
skus_to_check.extend(product["all_skus"])
# Nếu là sản phẩm lẻ, lấy SKU (chính là Product Color Code)
if "sku" in prod:
return [prod["sku"]]
# Fallback for raw results (legacy or defensive)
else:
sku = product.get("product_color_code") or product.get("magento_ref_code")
if sku:
skus_to_check.append(sku)
return []
for product in combined_results:
skus_to_check.extend(_extract_skus(product))
stock_map = {}
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:
stock_map = await fetch_stock_for_skus(skus_to_check)
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:
# Merge stock info into each product
for product in combined_results:
# Handle Flat Result
if "sku" in product:
sku = product["sku"]
if sku in stock_map:
product["stock_info"] = stock_map[sku]
product_skus = _extract_skus(product)
# Handle Grouped Result
elif "all_skus" in product:
# Aggregate stock for all variants
# For brevity, we can store a map or a summary.
# Let's store a map of {sku: stock_info}
group_stock = {}
has_stock = False
for s in product["all_skus"]:
if s in stock_map:
group_stock[s] = stock_map[s]
has_stock = True
if has_stock:
# If single SKU
if len(product_skus) == 1:
s = product_skus[0]
if s in stock_map:
product["stock_info"] = stock_map[s]
# If multiple SKUs (Grouped)
elif len(product_skus) > 1:
group_stock = {s: stock_map[s] for s in product_skus if s in stock_map}
if 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
final_info = all_filter_infos[0] if all_filter_infos else {}
......
import logging
import time
import os
from common.embedding_service import create_embedding_async
......@@ -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)."""
clauses = []
# 1. Exact Match
exact_fields = [
("gender_by_product", "gender_by_product"),
("age_by_product", "age_by_product"),
("form_neckline", "form_neckline"),
]
for param_name, col_name in exact_fields:
val = getattr(params, param_name, None)
if val:
clauses.append(f"{col_name} = %s")
sql_params.append(val)
# 2. Partial Match (LIKE)
# 1. Exact Match with Mapping
# Chúng ta sử dụng Mapping để chuyển từ Tiếng Việt (LLM) sang Tiếng Anh (DB Column)
from agent.tools.data_retrieval_filter import GENDER_MAP, AGE_MAP
# Gender Mapping
gender_val = getattr(params, "gender_by_product", None)
if gender_val:
gender_key = gender_val.lower().strip()
acceptable_genders = GENDER_MAP.get(gender_key, [gender_key])
placeholders = ", ".join(["%s"] * len(acceptable_genders))
clauses.append(f"gender_by_product IN ({placeholders})")
sql_params.extend(acceptable_genders)
# 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 = [
("season", "season"),
("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"),
("style", "style"),
("fitting", "fitting"),
......@@ -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
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]
# ============================================================
......@@ -121,48 +143,78 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
# Vector params
v_str = "[" + ",".join(str(v) for v in query_vector) + "]"
# Collect Params
price_params: list = []
price_clauses = _get_price_clauses(params, price_params)
# Collect All Filters
sql_params: list = []
# 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 = ""
if price_clauses:
where_filter = " AND " + " AND ".join(price_clauses)
logger.info(f"💰 [PRICE FILTER] Applied: {where_filter}")
if all_clauses:
where_filter = " AND " + " AND ".join(all_clauses)
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.
# Using %s for vector list string might cause StarRocks to treat it as string literal '[...]' instead of array.
sql = f"""
WITH top_matches AS (
SELECT /*+ SET_VAR(ann_params='{{"ef_search":128}}') */
internal_ref_code,
magento_ref_code,
product_color_code,
description_text_full,
product_name,
master_color,
product_image_url_thumbnail,
product_web_url,
sale_price,
original_price,
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
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE 1=1 {where_filter}
ORDER BY similarity_score DESC
LIMIT 200
LIMIT 100
)
SELECT
internal_ref_code,
MAX_BY(magento_ref_code, similarity_score) as magento_ref_code,
MAX_BY(product_color_code, similarity_score) as product_color_code,
MAX_BY(description_text_full, similarity_score) as description_text_full,
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(description_text_full, similarity_score) as description_text_full,
MAX(similarity_score) as max_score
FROM top_matches
WHERE 1=1 {where_filter}
GROUP BY internal_ref_code
GROUP BY product_color_code, internal_ref_code
ORDER BY max_score DESC
LIMIT 70
LIMIT 50
"""
# Return sql and params (params only contains filter values now, not the vector)
return sql, price_params
# 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"-- [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
from typing import Any
......@@ -161,10 +162,23 @@ async def check_stock(req: StockExpandRequest):
try:
stock_responses: list[dict[str, Any]] = []
async with httpx.AsyncClient(timeout=req.timeout_sec) as client:
tasks = []
for chunk in _chunked(ordered_skus, req.chunk_size):
resp = await client.get(STOCK_API_URL, params={"skus": ",".join(chunk)})
resp.raise_for_status()
stock_responses.append(resp.json())
tasks.append(client.get(STOCK_API_URL, params={"skus": ",".join(chunk)}))
# 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
return response_payload
except httpx.RequestError as exc:
......
......@@ -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 }
taskkill /F /IM python.exe
\ No newline at end of file
taskkill /F /IM python.exe
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