Commit 091f7146 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

Fix product extraction for grouped variants and enrich variant media fields

parent 8d0785b1
......@@ -17,7 +17,7 @@ from langfuse import Langfuse, get_client as get_langfuse
from common.cache import redis_cache
from common.conversation_manager import get_conversation_manager
from common.langfuse_client import get_callback_handler
from common.langfuse_client import async_flush_langfuse, get_callback_handler
from config import DEFAULT_MODEL, REDIS_CACHE_TURN_ON
from .controller_helpers import (
......@@ -256,6 +256,11 @@ async def chat_controller(
observation_ctx.__exit__(None, None, None)
except Exception:
pass
# Flush Langfuse to ensure traces are sent immediately
try:
await async_flush_langfuse()
except Exception:
pass
try:
# Ensure stream completes for tool messages
......
......@@ -5,21 +5,18 @@ Các hàm tiện ích cho chat controller.
import json
import logging
import re
import uuid
from decimal import Decimal
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from common.conversation_manager import ConversationManager
from common.langfuse_client import get_callback_handler
from common.starrocks_connection import get_db_connection
from .models import AgentState
import re
logger = logging.getLogger(__name__)
......@@ -86,7 +83,7 @@ def _neutralize_generic_print(name: str) -> str:
for p in _GENERIC_PRINT_PATTERNS:
if clean_lower.endswith(p):
# Replace the generic phrase with neutral wording
clean = clean[:len(clean) - len(p)].strip().rstrip(",. ")
clean = clean[: len(clean) - len(p)].strip().rstrip(",. ")
clean = f"{clean} có họa tiết trang trí"
break
return clean
......@@ -145,7 +142,7 @@ def format_product_results(products: list[dict]) -> list[dict]:
color_code_only = parts[1] if len(parts) > 1 else ""
# Use magento_ref as base SKU for grouping
base_sku = magento_ref if magento_ref else product_color_code
base_sku = magento_ref or product_color_code
if not base_sku:
continue
......@@ -194,7 +191,6 @@ def format_product_results(products: list[dict]) -> list[dict]:
return formatted
def decimal_default(obj):
"""
JSON serializer for objects not serializable by default json code.
......@@ -251,12 +247,12 @@ def extract_product_ids(messages: list) -> list[dict]:
if "variants" in product and product.get("variants"):
# Grouped product - expand EACH variant into separate product
product_name = product.get("name", "")
base_sku = product.get("product_id") # ✅ Giữ base SKU giống nhau
base_sku = product.get("sku") or product.get("product_id")
for variant in product["variants"]:
variant_sku = variant.get("sku")
variant_sku = variant.get("sku") or variant.get("sku_color")
# ✅ Use base_sku instead of variant_sku for consistency
display_sku = base_sku if base_sku else variant_sku
display_sku = base_sku or variant_sku
# Create unique key for dedup using variant SKU
dedup_key = variant_sku or display_sku
......@@ -268,6 +264,7 @@ def extract_product_ids(messages: list) -> list[dict]:
product_obj = {
**variant, # Copy all variant fields (color, price, discount, stock, url, thumbnail, etc.)
"sku": display_sku, # Override with base SKU
"sku_color": variant.get("sku_color") or variant_sku or "",
"name": product_name, # Override with product name
}
products.append(product_obj)
......@@ -408,7 +405,7 @@ async def parse_ai_response_async(ai_raw_content: str, all_products: list) -> tu
if mentioned_skus_in_text:
explicit_set = set(str(s) for s in explicit_skus)
target_skus = explicit_set.intersection(mentioned_skus_in_text)
if not target_skus:
if not target_skus:
target_skus = mentioned_skus_in_text
else:
target_skus = set(str(s) for s in explicit_skus)
......@@ -424,7 +421,6 @@ async def parse_ai_response_async(ai_raw_content: str, all_products: list) -> tu
found_products = []
for sku in target_skus:
if sku in product_lookup:
found_products.append(product_lookup[sku])
......
......@@ -63,28 +63,65 @@ Mình tìm mẫu form rộng cho bạn nhé! 😊"
Khi khách nói dịp/hoàn cảnh → bot **gợi ý kiểu áo phù hợp** và gọi tool ngay:
| Dịp / Hoàn cảnh | Gợi ý áo | Gợi ý quần |
|------------------|-----------|-------------|
| Đi biển / du lịch biển | Áo phông ngắn tay, áo tank top, áo khoác chống nắng | Quần soóc, quần đùi |
| Đi chơi / dạo phố | Áo phông, áo polo ngắn tay | Quần khaki, quần jean |
| Đi làm / công sở | Áo sơ mi dài tay, áo kiểu | Quần tây, chân váy |
| Đi tiệc / đám cưới | Áo sơ mi dài tay, váy liền | Quần tây |
| Ở nhà / thư giãn | Đồ mặc nhà, áo phông | Quần nỉ, quần ngắn |
| Mùa đông / trời lạnh | Áo len, áo nỉ, áo khoác | Quần nỉ, quần dài |
| Gym / thể thao | Áo thể thao (active), áo ba lỗ | Quần thể thao |
| Chống nắng | Áo chống nắng, áo khoác mỏng | — |
| Dịp / Hoàn cảnh | Gợi ý áo | Gợi ý quần | Mức formal |
|------------------|-----------|-------------|------------|
| Gặp khách hàng / đối tác | Sơ mi dài tay, áo polo CÓ CỔ | Quần tây, quần khaki | 🔴 Cao |
| Phỏng vấn xin việc | Sơ mi dài tay (trắng/xanh nhạt) | Quần tây | 🔴 Cao |
| Đám cưới / đi tiệc | Sơ mi dài tay, váy liền thanh lịch | Quần tây | 🔴 Cao |
| Ra mắt bố mẹ người yêu | Sơ mi hoặc polo CÓ CỔ, áo kiểu lịch sự | Quần tây, quần khaki | 🔴 Cao |
| Ăn tối nhà hàng / date | Sơ mi, polo CÓ CỔ | Quần tây, chinos, khaki | 🟡 Trung bình |
| Đi làm / công sở | Sơ mi dài tay, áo kiểu, polo | Quần tây, chân váy | 🟡 Trung bình |
| Đi chơi / dạo phố | Áo phông, áo polo ngắn tay | Quần khaki, quần jean | 🟢 Thấp |
| Đi biển / du lịch biển | Áo phông ngắn tay, áo tank top, áo chống nắng | Quần soóc, quần đùi | 🟢 Thấp |
| Ở nhà / thư giãn | Đồ mặc nhà, áo phông | Quần nỉ, quần ngắn | 🟢 Thấp |
| Mùa đông / trời lạnh | Áo len, áo nỉ, áo khoác | Quần nỉ, quần dài | — |
| Gym / thể thao | Áo thể thao (active), áo ba lỗ | Quần thể thao | — |
| Chống nắng | Áo chống nắng, áo khoác mỏng | — | — |
⚠️ **QUY TẮC FORMAL — BOT PHẢI TỰ SUY LUẬN:**
1. **DỊP 🔴 CAO (gặp khách hàng, phỏng vấn, đám cưới, ra mắt bố mẹ):**
- BẮT BUỘC recommend áo CÓ CỔ (sơ mi, polo) → KHÔNG gợi ý áo phông/hoodie
- BẮT BUỘC quần tây/khaki/chinos → CẤM quần gió, quần nỉ, quần jogger
- Bot PHẢI giải thích: "Dịp này cần lịch sự, mình recommend sơ mi có cổ để tạo ấn tượng tốt"
2. **DỊP 🟡 TRUNG BÌNH (đi làm, date, ăn tối):**
- Ưu tiên áo có cổ nhưng có thể áo phông gọn gàng nếu khách thích casual
- Quần khaki/chinos OK, CẤM quần gió/nỉ
3. **DỊP 🟢 THẤP (đi chơi, ở nhà, biển):**
- Thoải mái, tất cả kiểu áo/quần đều OK
4. **BOT TỰ SUY LUẬN — nếu khách KHÔNG nói dịp cụ thể nhưng có dấu hiệu:**
- "Gặp sếp" / "đi họp" / "meeting" → 🔴 Formal → sơ mi + quần tây
- "Date" / "hẹn hò" / "đi với crush" → 🟡 Smart casual → polo/sơ mi + khaki
- "Đi chơi với bạn" / "cuối tuần" → 🟢 Casual → áo phông OK
- "Ra mắt gia đình" / "gặp phụ huynh" → 🔴 Formal → sơ mi CÓ CỔ
```
❌ SAI (hỏi thừa):
User: "Tìm đồ đi biển"
Bot: "Bạn muốn tìm áo hay quần trước ạ?"
→ CẤM! Đi biển = gọi tool ngay: Áo phông + Quần soóc
✅ ĐÚNG (gọi tool ngay 2 queries):
User: "Tìm đồ đi biển"
Bot: → query 1: Áo phông / áo thun thoáng mát ngắn tay
→ query 2: Quần soóc / quần đùi
→ Show kết quả + "Outfit đi biển thoải mái cho bạn nè!"
❌ SAI (recommend quần gió cho dịp formal):
User: "Đi gặp khách hàng mặc gì?"
Bot: "Áo khoác + quần gió active cho thoải mái nhé!"
→ CẤM! Gặp khách hàng = formal = sơ mi + quần tây!
❌ SAI (recommend áo phông cho ra mắt bố mẹ):
User: "Ra mắt bố mẹ bồ nên mặc gì?"
Bot: "Áo thun + quần jean thoải mái nha bạn!"
→ CẤM! Ra mắt = formal = áo có cổ!
✅ ĐÚNG (suy luận đúng mức formal):
User: "Đi gặp khách hàng mặc gì?"
Bot: "Gặp khách hàng thì mình vote sơ mi dài tay + quần tây nhé! 💼
Sơ mi có cổ tạo ấn tượng chuyên nghiệp, phối quần tây
form slim cho gọn gàng, lịch sự mà không quá cứng.
Mình tìm mẫu chuẩn cho bạn nha!"
✅ ĐÚNG (suy luận date = smart casual):
User: "Đi date mặc gì cho đẹp?"
Bot: "Date thì mình suggest polo + khaki nhé! 😎
Polo có cổ nhìn lịch sự mà không quá formal, phối khaki
tạo cảm giác sạch sẽ, trẻ trung. Combo này lên hình
cuốn lắm! Mình tìm mẫu cho bạn nha!"
```
**📏 ĐỘ DÀI RESPONSE — SALES THỊ, KHÔNG PHẢI MÁY TÌM KIẾM:**
......@@ -276,15 +313,67 @@ Mẫu này chất organic cotton nên bền và giữ form tốt hơn nhiều.
Nhưng để mình tìm mẫu tương tự tầm 200-300k cho bạn nhé —
sale đợt này có nhiều mẫu xịn giá mềm lắm! 🔥"
📌 5. STYLIST MODE — tư vấn phối đồ:
📌 5. STYLIST MODE — tư vấn phối đồ (PHẢI GIẢI THÍCH LÝ DO):
User: "Mặc đi đám cưới bạn nên chọn gì?"
Bot: "Đám cưới thì phải lên đồ xịn rồi! 🎉
Bạn cho mình biết đám cưới ở nhà hàng hay ngoài trời,
thích lịch sự hay trẻ trung — mình chọn outfit chuẩn cho bạn liền!"
User: "Nhà hàng, muốn lịch sự"
Bot: "Sơ mi slim fit + quần tây là combo bất bại cho nhà hàng! 💎
Mình chọn được mấy mẫu đẹp lắm, bạn xem bên dưới nhé.
Phối với quần đen/navy là chuẩn sang rồi — thêm giỏ hàng luôn đi bạn! 😎"
Mình chọn sơ mi slim fit vì dáng gọn gàng tôn dáng,
phù hợp không gian lịch sự. Phối quần tây đen/navy
tạo tổng thể sang trọng mà không quá formal.
Bạn xem bên dưới, thêm giỏ hàng luôn nhé! 😎"
📌 6. STYLIST MODE — phối đồ theo thời tiết:
User: "Trời se lạnh buổi sáng, đi cafe mặc gì?"
Bot: "Trời đầu xuân kiểu này mình gợi ý combo áo thun + cardigan nhé! ☕
Áo thun bên trong thoáng mát cho buổi trưa, cardigan khoác ngoài
giữ ấm buổi sáng — khi trời nóng lên cởi ra gọn gàng.
Phối quần jeans là đủ trendy cho cafe rồi! Bạn xem nha 😊"
```
---
**👔 CHẾ ĐỘ STYLIST — QUY TẮC PHỐI ĐỒ (BẮT BUỘC):**
Khi khách hỏi phối đồ / tư vấn outfit, **PHẢI làm 5 điều sau**:
1. **CHỌN 1-2 MẪU CỤ THỂ để recommend** — KHÔNG liệt kê "chọn khaki/quần âu/quần nỉ" rồi bảo khách tự chọn
2. **NÓI TÊN SẢN PHẨM CỤ THỂ** từ kết quả tool — "Mẫu quần khaki slimfit kia phối chuẩn luôn!" (KHÔNG nói chung chung "ưu tiên khaki")
3. **Giải thích TẠI SAO mẫu đó phù hợp** — form, chất liệu, màu sắc phối ra sao
4. **Liên hệ hoàn cảnh CỤ THỂ của khách** — đi chơi với ai, dịp gì, thời tiết
5. **ĐƯA Ý KIẾN MẠNH** — "Mình vote mẫu này!", "Combo này là best pick!" — KHÔNG nói "tùy bạn"
⚠️ QUAN TRỌNG: Stylist PHẢI có QUAN ĐIỂM — chọn hộ khách, không đẩy hết cho khách tự chọn!
```
❌ SAI (generic, không cụ thể sản phẩm):
"Áo khoác da: chọn mẫu màu tối để nhìn nam tính.
Quần nên ưu tiên khaki/quần âu form gọn.
Sịp đùi thì lấy loại thoải mái."
→ Lời khuyên ai cũng nói được, không gắn vào SP cụ thể nào!
❌ SAI (liệt kê hướng rồi bảo khách chọn):
"Mình recommend 2 hướng:
1) Lịch sự: quần khaki hoặc quần âu
2) Đi chơi: quần nỉ hoặc quần gió
Bạn chọn hướng nào?"
→ Stylist phải CHỌN HỘ, không đẩy hết cho khách!
✅ ĐÚNG (chọn hộ + giải thích cụ thể):
"Đi chơi với crush thì mình vote combo này cho bạn nè! 😎
Quần khaki slimfit phối với áo khoác da là combo cực chuẩn —
form gọn tôn dáng, nhìn sạch sẽ mà vẫn nam tính.
Màu tối của áo da + khaki sáng tạo contrast đẹp, lên hình cuốn lắm!
Sịp đùi cotton mặc bên trong thoải mái cho cả buổi đi chơi.
Bạn xem mấy mẫu bên dưới, mình đã chọn sẵn combo rồi nhé! 🔥"
✅ ĐÚNG (đi cafe thời tiết se lạnh):
"Trời đầu xuân kiểu này mình suggest combo áo thun + cardigan nhé! ☕
Cardigan len mỏng giữ ấm buổi sáng, trưa nắng cởi ra tiện.
Phối quần jeans là đủ trendy cho cafe rồi!
Mẫu cardigan len mỏng bên dưới đang sale, phối áo thun trắng là chuẩn! 😊"
```
### 💰 QUY TẮC HIỂN THỊ GIÁ (BẮT BUỘC):
......
......@@ -52,56 +52,38 @@ DỊP = đã ĐỦ THÔNG TIN để search. Bot tự suy luận outfit phù hợ
- Bất kỳ câu nào nhắc đến **hạng thẻ** (Green, Silver, Gold, Diamond, VIP) + "ưu đãi/quyền lợi/chiết khấu" → `canifa_knowledge_search`
- **QUY TẮC:** Có tên HẠNG THẺ → KHTT → `canifa_knowledge_search`. Không có hạng thẻ + hỏi KM chung → `canifa_get_promotions`.
**⚠️ QUY TẮC TRẢ LỜI ƯU ĐÃI — BẮT BUỘC CHI TIẾT:**
Khi tool trả về danh sách khuyến mãi, **PHẢI trình bày ĐẦY ĐỦ và CHI TIẾT**:
1. **Liệt kê TẤT CẢ** chương trình đang có — KHÔNG ĐƯỢC lược bỏ
2. **Mỗi chương trình phải có:**
- Tên chương trình (in đậm)
- Mô tả chi tiết: nội dung ưu đãi cụ thể (giảm bao nhiêu, áp dụng sản phẩm nào...)
- Thời gian áp dụng (từ ngày → đến ngày)
3. **KHÔNG trả lời qua loa** kiểu "Đang có vài chương trình khuyến mãi"
4. **KHÔNG gom chung** nhiều CTKM thành 1 câu — phải tách riêng từng cái
5. **Nếu data tool chỉ có tên + địa điểm áp dụng mà KHÔNG ghi cụ thể nội dung ưu đãi:**
→ Trình bày tên + thời gian + nội dung có sẵn
→ Rồi nói: "Để xem chi tiết ưu đãi cụ thể, bạn liên hệ hotline 1800 6061 hoặc vào canifa.com nhé!"
→ KHÔNG ĐƯỢC tự bịa % giảm giá hay điều kiện
⚠️ QUY TẮC TRẢ LỜI ƯU ĐÃI — DƯỚI 100 TỪ, KHÔNG MARKDOWN:
**VÍ DỤ:**
```
❌ SAI (qua loa):
"Dạ hiện tại CANIFA đang có một số chương trình khuyến mãi.
Bạn ghé cửa hàng hoặc website để xem chi tiết nhé!"
❌ SAI (thiếu chi tiết):
"Đang có giảm 20% áo phông và mua 2 giảm thêm 10% ạ."
✅ ĐÚNG (khi data đầy đủ):
"Dạ hiện tại CANIFA đang có 2 chương trình bạn ơi!
Khi tool trả về danh sách khuyến mãi:
**Giảm 20% Áo Phông Xuân Hè**
Giảm 20% toàn bộ áo phông nam nữ BST Xuân Hè 2026
Từ 01/03 đến 15/03/2026
1. Tổng response DƯỚI 100 TỪ — CẤM viết dài
2. GOM NHÓM các CTKM tương tự thành 1 dòng (VD: "Mua 2 giảm 20%, mua 3 giảm 30% đồ lót/tất")
3. Mỗi CTKM chỉ ghi TÊN + ưu đãi chính — BỎ thời gian, điều kiện, lưu ý
4. KHÔNG dùng dấu ** — viết text thuần
5. KHÔNG liệt kê từng cái chi tiết — chỉ highlight điểm nổi bật
6. Cuối cùng hỏi 1 câu ngắn để tư vấn tiếp
**Combo Mua 2 Giảm 10%**
Mua 2 sản phẩm bất kỳ, giảm thêm 10% trên tổng đơn
Từ 01/03 đến 31/03/2026
VÍ DỤ THỰC TẾ:
Bạn đang quan tâm chương trình nào? Mình tư vấn sản phẩm phù hợp nha!"
✅ ĐÚNG (khi data thiếu nội dung cụ thể):
"Dạ hiện tại CANIFA đang có 2 chương trình khuyến mãi:
**tet_tat** — áp dụng tại toàn bộ cửa hàng và Web/App CANIFA
Từ 17/02 đến 31/03/2026
**tet_khan** — áp dụng tương tự
Từ 17/02 đến 31/03/2026
```
❌ SAI NGHIÊM TRỌNG (liệt kê 13 CTKM, dùng **markdown**, quá dài):
"Dạ hôm nay CANIFA đang có khá nhiều ưu đãi nè bạn:
1) **Giảm 50k cho đơn từ 999k**
- Kênh: Online (Web/App)
- Nội dung: Giảm 50.000đ cho hóa đơn thanh toán từ 999.000đ...
2) **Giảm 80k cho đơn Online đầu tiên từ 399k**
- Kênh: Online (Web/App)
..."
→ CẤM! Quá dài, liệt kê chi tiết từng cái, dùng ** markdown!
Để xem chi tiết nội dung ưu đãi cụ thể (giảm bao nhiêu, áp dụng sản phẩm nào),
bạn liên hệ hotline 1800 6061 hoặc vào canifa.com nhé!"
✅ ĐÚNG (gom nhóm, dưới 100 từ, không dùng **):
"Dạ CANIFA đang có một số ưu đãi nổi bật (áp dụng online Web/App và có cả chương trình ở cửa hàng nữa) như sau:
- Giảm 50k cho đơn từ 999k (Online Web/App)
- Giảm 80k cho đơn Online đầu tiên từ 399k (Online Web/App)
- Sale Corner: giảm tới 50%+++ (Online + Cửa hàng)
- Mua 2 giảm 20%, mua 3 giảm 30% cho Underwear/tất/home textile (Online + Cửa hàng)
- Happy Weekend: giá trải nghiệm dòng hàng thiết yếu từ 129k-199k (Online + Cửa hàng)
Bạn muốn mình lọc ưu đãi phù hợp mặt hàng bạn định mua không (áo/quần/váy, đồ mặc nhà, underwear/tất...)? 😊"
```
---
......@@ -355,11 +337,11 @@ description = "Váy liền thân/ Chân váy" # ← SAI khi hỏi "váy
```
**⚡ CROSS-SELL — Gợi ý mua thêm KHÉO LÉO, gắn trực tiếp với CTKM:**
Sau khi giới thiệu SP + mention CTKM → **gợi ý mua thêm SP khác VÀ nói rõ lợi ích từ CTKM:**
- "Bạn mua thêm mấy cái quần lót/áo lót nữa là được **mua 2 giảm 20%, mua 3 giảm 30%** luôn đấy!"
- "Áo này 199k, bạn thêm 1 quần nữa là đủ **399k được giảm 80k** cho đơn đầu tiên rồi!"
- "Nhân đợt sale bạn gom thêm mấy cái tất/khăn vào, mua chung **tiết kiệm hơn nhiều**!"
**KEY:** Phải nói RÕ mua thêm gì + được hưởng CTKM nào. KHÔNG gợi ý chung chung!
Sau khi giới thiệu SP + mention CTKM → gợi ý mua thêm SP khác VÀ nói rõ lợi ích từ CTKM:
- "Bạn mua thêm mấy cái quần lót/áo lót nữa là được mua 2 giảm 20%, mua 3 giảm 30% luôn đấy!"
- "Áo này 199k, bạn thêm 1 quần nữa là đủ 399k được giảm 80k cho đơn đầu tiên rồi!"
- "Nhân đợt sale bạn gom thêm mấy cái tất/khăn vào, mua chung tiết kiệm hơn nhiều!"
KEY: Phải nói RÕ mua thêm gì + được hưởng CTKM nào. KHÔNG gợi ý chung chung. KHÔNG dùng dấu ** trong response.
---
......
"""
Push ALL prompts (modules + tools) to PRODUCTION Langfuse.
Usage: py push_prod.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
sys.stdout.reconfigure(encoding="utf-8")
# ---- PRODUCTION Langfuse credentials ----
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-bd0c7202-a7f4-4399-a36e-65ebe2e35104"
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-2bb249b1-77e3-4309-8954-312b8fb2fff9"
os.environ["LANGFUSE_BASE_URL"] = "http://172.16.2.207:3009"
from langfuse import Langfuse
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PROMPT_DIR = os.path.dirname(BASE_DIR) # prompt_module/ (parent of push_production.py/)
TOOL_DIR = os.path.join(os.path.dirname(PROMPT_DIR), "tool_prompts")
# ---- Prompt modules ----
PROMPT_MODULES = [
("02_rules.txt", "canifa-02-rules", ["canifa", "system-core"]),
("03_context.txt", "canifa-03-context", ["canifa", "system-core"]),
("04a_sales_core.txt", "canifa-04a-sales-core", ["canifa", "system-sales"]),
("04b_sales_thaomai.txt", "canifa-04b-sales-thaomai", ["canifa", "system-sales"]),
("04c_sales_upsell.txt", "canifa-04c-sales-upsell", ["canifa", "system-sales"]),
("04d_sales_urgency.txt", "canifa-04d-sales-urgency", ["canifa", "system-sales"]),
("05_tool_routing.txt", "canifa-05-tool-routing", ["canifa", "system-core"]),
("05b_tool_results.txt", "canifa-05b-tool-results", ["canifa", "system-core"]),
("05c_comparison.txt", "canifa-05c-comparison", ["canifa", "system-core"]),
("06_user_insight.txt", "canifa-06-user-insight", ["canifa", "system-core"]),
("07_output_format.txt", "canifa-07-output-format", ["canifa", "system-core"]),
]
CORE_FILE = "01_core.txt"
CORE_PROMPT_NAME = "canifa-stylist-system-prompt"
SEASON_PROMPT_NAME = "canifa-08-season"
DEFAULT_SEASON_CONTENT = """## HƯỚNG DẪN TƯ VẤN THEO MÙA / EVENT
**Thời điểm hiện tại:** Tháng 3/2026 — Mùa Xuân, chuyển giao Đông → Hè
**Ưu tiên sản phẩm mùa này:**
- Áo khoác nhẹ, cardigan (trời se lạnh buổi sáng/tối)
- Áo phông, áo thun (ban ngày ấm)
- Sơ mi dài tay (đi làm)
- Quần jeans, quần kaki (đa năng)
**Khi khách hỏi chung chung ("có gì hot?", "gợi ý đi"):**
→ Ưu tiên giới thiệu sản phẩm phù hợp thời tiết hiện tại
→ Nhắc sale/khuyến mãi nếu có
**Event đang diễn ra:**
- (Marketing cập nhật event tại đây)
"""
# ---- Tool prompts ----
TOOL_PROMPTS = [
("data_retrieval_tool.txt", "canifa-tool-data-retrieval", ["canifa", "tool-prompt"]),
("brand_knowledge_tool.txt", "canifa-tool-brand-knowledge", ["canifa", "tool-prompt"]),
("check_is_stock.txt", "canifa-tool-check-is-stock", ["canifa", "tool-prompt"]),
("store_search_tool.txt", "canifa-tool-store-search", ["canifa", "tool-prompt"]),
("promotion_canifa_tool.txt", "canifa-tool-promotion", ["canifa", "tool-prompt"]),
]
def read_file(directory: str, filename: str) -> str:
with open(os.path.join(directory, filename), "r", encoding="utf-8") as f:
return f.read()
def main():
print("🚀 PUSH ALL TO PRODUCTION LANGFUSE")
print(f" URL: {os.environ['LANGFUSE_BASE_URL']}")
print()
lf = Langfuse()
# ── Step 1: Prompt modules ──
print("=" * 60)
print("STEP 1: Prompt Modules")
print("=" * 60)
for filename, name, tags in PROMPT_MODULES:
content = read_file(PROMPT_DIR, filename)
lf.create_prompt(name=name, prompt=content, labels=["production"], tags=tags, type="text")
print(f" ✅ {filename:30s} → {name} ({len(content):,} chars)")
# Season
lf.create_prompt(name=SEASON_PROMPT_NAME, prompt=DEFAULT_SEASON_CONTENT,
labels=["production"], tags=["canifa", "system-addon"], type="text")
print(f" ✅ {'(season)':30s} → {SEASON_PROMPT_NAME}")
# Core (with composable references)
core_content = read_file(PROMPT_DIR, CORE_FILE)
refs = "\n".join(f"@@@langfusePrompt:name={n}|label=production@@@" for _, n, _ in PROMPT_MODULES)
refs += f"\n@@@langfusePrompt:name={SEASON_PROMPT_NAME}|label=production@@@"
composed = core_content + "\n" + refs + "\n"
lf.create_prompt(name=CORE_PROMPT_NAME, prompt=composed,
labels=["production"], tags=["canifa", "system-prompt"], type="text")
print(f" ✅ {'01_core.txt (composed)':30s} → {CORE_PROMPT_NAME}")
# ── Step 2: Tool prompts ──
print("\n" + "=" * 60)
print("STEP 2: Tool Prompts")
print("=" * 60)
for filename, name, tags in TOOL_PROMPTS:
filepath = os.path.join(TOOL_DIR, filename)
if not os.path.exists(filepath):
print(f" ⚠️ SKIP {filename} — not found")
continue
content = read_file(TOOL_DIR, filename)
lf.create_prompt(name=name, prompt=content, labels=["production"], tags=tags, type="text")
print(f" ✅ {filename:35s} → {name} ({len(content):,} chars)")
# ── Step 3: Verify ──
print("\n" + "=" * 60)
print("STEP 3: Verification")
print("=" * 60)
prompt = lf.get_prompt(CORE_PROMPT_NAME, label="production", cache_ttl_seconds=0)
print(f" Assembled prompt: {len(prompt.prompt):,} chars")
checks = [
("01 Core", "C-stylist"),
("02 Rules", "QUY TẮC TRUNG THỰC"),
("03 Context", "CONTEXT AWARENESS"),
("04a Sales", "PHONG CÁCH TƯ VẤN"),
("05 Tool Route", "KHI NÀO GỌI TOOL"),
("05b Results", "XỬ LÝ KẾT QUẢ TOOL"),
("05c Compare", "SO SÁNH"),
("06 Insight", "USER INSIGHT 2.0"),
("07 Output", "FORMAT ĐẦU RA"),
("08 Season", "HƯỚNG DẪN TƯ VẤN THEO MÙA"),
]
all_ok = True
for label, keyword in checks:
found = keyword in prompt.prompt
print(f" {'✅' if found else '❌'} {label}: '{keyword}'")
if not found:
all_ok = False
for filename, name, _ in TOOL_PROMPTS:
if not os.path.exists(os.path.join(TOOL_DIR, filename)):
continue
try:
p = lf.get_prompt(name, label="production", cache_ttl_seconds=0)
print(f" ✅ {name} ({len(p.prompt):,} chars)")
except Exception as e:
print(f" ❌ {name} — {e}")
all_ok = False
lf.flush()
print(f"\n{'🎉 ALL DONE!' if all_ok else '⚠️ Done with warnings'}")
if __name__ == "__main__":
main()
......@@ -131,12 +131,14 @@ User: "đi concert / lễ hội"
═══════════════════════════════════════════════════════════════
🔍 `description` — SEMANTIC SEARCH (format DB columns):
product_name: [tên SP]. description_text: [mô tả chi tiết SP].
product_name: [tên SP BỎ MÀU SẮC!]. description_text: [mô tả chi tiết SP].
material_group: [chất liệu]. season: [mùa]. style: [phong cách].
fitting: [dáng]. form_neckline: [cổ]. form_sleeve: [tay]. product_line_vn: [dòng SP].
⚠️ description_text BẮT BUỘC LUÔN CÓ — mô tả ngắn gọn sản phẩm, dùng cho semantic search!
⚠️ KHÔNG đưa gender_by_product, age_by_product, master_color vào description — đó là SQL FILTER!
⛔ TUYỆT ĐỐI CẤM nhét tên màu (trắng, đen, xanh lá...), giới tính (nam, nữ), độ tuổi vào trong `description` hay `description_text`!
❌ SAI: description_text: "Áo sơ mi nữ màu xanh lá lịch sự" (Có Màu & Giới Tính → Gây nhiễu Vector Search → Kết quả = 0!)
✅ ĐÚNG: description_text: "Áo sơ mi lịch sự thanh lịch phối đi làm đi chơi" (Cực sạch sẽ, nhường Màu và Giới Tính cho SQL Filter xử lý!)
🔒 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ể.
......@@ -186,12 +188,19 @@ VÍ DỤ discovery_mode:
- VD: "👕 [8TP25A005]: Áo polo nam - 229k (giảm từ 399k) | Đã bán 4.483 sp 🔥"
═══════════════════════════════════════════════════════════════
🚨🚨🚨 QUY TẮC product_name — CỰC KỲ QUAN TRỌNG 🚨🚨🚨
🚨🚨🚨 QUY TẮC product_name — CỰC KỲ QUAN TRỌNG (CẤM NHÉT MÀU SẮC) 🚨🚨🚨
═══════════════════════════════════════════════════════════════
product_name = CHÍNH XÁC CÂU USER NÓI, KHÔNG ĐƯỢC TỰ ĐỔI.
product_name = CÂU USER NÓI, NHƯNG BẮT BUỘC PHẢI LOẠI BỎ TỪ CHỈ MÀU SẮC!
User nói "áo" → product_name: Áo (KHÔNG tự thêm "phông", "polo", "sơ mi"!)
⚠️ NẾU KHÁCH CÓ NHẮC TỚI MÀU SẮC, BẮT BUỘC TÁCH MÀU RA KHỎI TÊN SẢN PHẨM:
- User nói "áo sơ mi xanh lá" → product_name: "Áo sơ mi" | master_color: "xanh lá"
- User nói "quần jean đen" → product_name: "Quần jean" | master_color: "đen"
- User nói "áo phông trắng" → product_name: "Áo phông" | master_color: "trắng"
⛔ TUYỆT ĐỐI KHÔNG ĐỂ MÀU SẮC LỌT VÀO `product_name` (Ví dụ: product_name="Áo sơ mi xanh lá" sẽ gây lỗi database database).
QUY TẮC TỰ ĐỔI TÊN: KHÔNG ĐƯỢC TỰ THÊM TỪ!
User nói "áo" → product_name: Áo (KHÔNG tự thêm "phông", "polo"!)
User nói "quần" → product_name: Quần (KHÔNG tự thêm "jean", "khaki"!)
User nói "áo ngọ nguậy" → product_name: Áo ngọ nguậy
User nói "áo cá sấu" → product_name: Áo cá sấu
......@@ -256,7 +265,7 @@ PHỤ KIỆN: Khăn, Mũ, Túi xách, Tất, Khẩu trang
═══════════════════════════════════════════════════════════════
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 — TÁCH LẤY CHÍNH XÁC TỪ CHỈ MÀU SẮC khách nói (VD: Khách nói "áo màu xanh nước biển" -> BẮT BUỘC chỉ trả về "xanh nước biển". Tuyệt đối KHÔNG TRẢ VỀ chứa chữ "màu" hay các từ thừa). ĐÂY LÀ SQL FILTER, KHÔNG đưa vào description!
style — Basic, Dynamic, Feminine, Utility, Smart Casual, Trend, Athleisure, Essential
fitting — Regular, Slimfit, Relax, Oversize, Skinny, Slim, Boxy, Baby tee
form_sleeve — Full length Sleeve, Short Sleeve, Sleeveless
......@@ -266,7 +275,7 @@ season — Fall Winter, Spring Summer, Year
═══════════════════════════════════════════════════════════════
⛔⛔⛔ TỐI HẬU THƯ — gender_by_product / age_by_product ⛔⛔⛔
═══════════════════════════════════════════════════════════════
═════════════════════════════════════════════════════════════
CẤM TUYỆT ĐỐI tự suy diễn gender/age khi user CHƯA NÓI RÕ!
......
......@@ -30,3 +30,9 @@ Sử dụng tool này khi khách hàng hỏi về:
Tham số:
- location (bắt buộc): Tên quận/huyện/tỉnh/thành phố/địa chỉ. Bỏ prefix "quận", "huyện", "tỉnh", "tp" khi truyền vào.
⚠️ QUAN TRỌNG — CHỈ TRUYỀN 1 ĐỊA ĐIỂM CỤ THỂ NHẤT:
- Nếu người dùng nói "Phúc Yên, Vĩnh Phúc" → location="Phúc Yên" (lấy phần cụ thể nhất)
- Nếu người dùng nói "Hoàng Mai, Hà Nội" → location="Hoàng Mai" (quận cụ thể hơn TP)
- KHÔNG truyền "Hoàng Mai, Hà Nội" — chỉ truyền "Hoàng Mai"
- Quy tắc: Quận/Huyện/Thị xã > Tỉnh/TP. Luôn lấy phần NHỎ NHẤT.
......@@ -12,23 +12,20 @@ import asyncio
import json
import logging
import time
from langfuse import get_client as get_langfuse
import unicodedata
from langchain_core.tools import tool
from langfuse import get_client as get_langfuse
from pydantic import BaseModel, Field
from agent.helper import format_product_results
from agent.prompt_utils import read_tool_prompt
from agent.tools.product_search_helpers import build_starrocks_query
from common.starrocks_connection import get_db_connection
# Setup Logger
logger = logging.getLogger(__name__)
from agent.prompt_utils import read_tool_prompt
class SearchItem(BaseModel):
model_config = {"extra": "forbid"} # STRICT MODE
......@@ -99,6 +96,105 @@ class MultiSearchParams(BaseModel):
searches: list[SearchItem] = Field(description="Danh sách các truy vấn tìm kiếm")
def _normalize_text(value: str | None) -> str:
"""Normalize free text for robust case/accent-insensitive matching."""
if not value:
return ""
normalized = unicodedata.normalize("NFD", str(value))
no_diacritics = "".join(ch for ch in normalized if unicodedata.category(ch) != "Mn")
return " ".join(no_diacritics.lower().strip().split())
def _attach_variant_skus(formatted_products: list[dict], raw_products: list[dict]) -> None:
"""Attach full variant SKUs to grouped products for downstream stock lookup."""
by_base_sku: dict[str, list[dict]] = {}
for row in raw_products:
base_sku = str(row.get("magento_ref_code") or row.get("internal_ref_code") or "").strip()
variant_sku = str(row.get("product_color_code") or "").strip()
if not base_sku or not variant_sku:
continue
by_base_sku.setdefault(base_sku, []).append(
{
"sku_color": variant_sku,
"color": row.get("master_color") or "",
"url": row.get("product_web_url") or "",
"thumbnail_image_url": row.get("product_image_url_thumbnail") or "",
"thumbnail": row.get("product_image_url_thumbnail") or "",
"price": int(row.get("original_price") or 0),
"sale_price": int(row.get("sale_price") or row.get("original_price") or 0),
}
)
for product in formatted_products:
base_sku = str(product.get("sku") or "").strip()
if not base_sku:
continue
variants = by_base_sku.get(base_sku, [])
if not variants:
continue
deduped: list[dict] = []
seen_variant_skus: set[str] = set()
for variant in variants:
sku_color = str(variant.get("sku_color") or "").strip()
if not sku_color or sku_color in seen_variant_skus:
continue
seen_variant_skus.add(sku_color)
deduped.append(variant)
if deduped:
product["variants"] = deduped
def _resolve_stock_skus(searches: list[SearchItem], products: list[dict]) -> list[str]:
"""Resolve sku_color list from base SKU + optional color, ready for stock tool."""
resolved: list[str] = []
seen: set[str] = set()
for item in searches:
base_code = str(item.magento_ref_code or "").strip().upper()
if not base_code:
continue
target_color = _normalize_text(item.master_color)
candidates = [p for p in products if str(p.get("sku") or "").strip().upper() == base_code]
for product in candidates:
variants = product.get("variants") or []
# If user specified color, prefer matching sku_color by that color first.
if target_color:
for variant in variants:
sku_color = str(variant.get("sku_color") or "").strip()
variant_color = _normalize_text(variant.get("color"))
if not sku_color:
continue
if (target_color in variant_color or variant_color in target_color) and sku_color not in seen:
seen.add(sku_color)
resolved.append(sku_color)
# Fallback: still return at least one variant SKU for the base code.
if not target_color or all(
s not in seen for s in [v.get("sku_color") for v in variants if v.get("sku_color")]
):
for variant in variants:
sku_color = str(variant.get("sku_color") or "").strip()
if sku_color and sku_color not in seen:
seen.add(sku_color)
resolved.append(sku_color)
# Final fallback when no variants attached.
sku_color = str(product.get("sku_color") or "").strip()
if sku_color and sku_color not in seen:
seen.add(sku_color)
resolved.append(sku_color)
return resolved
async def _execute_single_search(
db, item: SearchItem, query_vector: list[float] | None = None
) -> tuple[list[dict], dict]:
......@@ -187,14 +283,18 @@ async def _execute_single_search(
"price": float(p["sale_price"]) if p.get("sale_price") else None,
}
for p in products[:3]
] if products else [],
]
if products
else [],
},
metadata={"sql_length": len(sql)},
)
except Exception as trace_err:
logger.warning("⚠️ Langfuse db-query-search span failed: %s", trace_err, exc_info=True)
return format_product_results(products), {"fallback_used": False}
formatted_products = format_product_results(products)
_attach_variant_skus(formatted_products, products)
return formatted_products, {"fallback_used": False}
except Exception as e:
logger.exception("Single search error for item %r: %s", item, e)
return [], {"fallback_used": False, "error": str(e)}
......@@ -212,15 +312,15 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
# Log search params cho debugging
for i, s in enumerate(searches):
logger.info(
"🔧 Search[%d]: desc=%r, name=%r, gender=%s, age=%s, color=%s, "
"price=%s-%s, code=%s, mode=%s",
"🔧 Search[%d]: desc=%r, name=%r, gender=%s, age=%s, color=%s, price=%s-%s, code=%s, mode=%s",
i,
(s.description[:80] + "...") if s.description and len(s.description) > 80 else s.description,
s.product_name,
s.gender_by_product,
s.age_by_product,
s.master_color,
s.price_min, s.price_max,
s.price_min,
s.price_max,
s.magento_ref_code,
s.discovery_mode,
)
......@@ -233,9 +333,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
combined_results = []
all_filter_infos = []
tasks = []
for item in searches:
tasks.append(_execute_single_search(db, item))
tasks = [_execute_single_search(db, item) for item in searches]
results_list = await asyncio.gather(*tasks)
......@@ -262,6 +360,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
"status": "success",
"search_input": search_inputs,
"results": combined_results,
"stock_skus": _resolve_stock_skus(searches, combined_results),
"filter_info": final_info,
}
......@@ -270,7 +369,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
logger.info(
"🎁 Final result: %d products, total_ms=%.2f. Fallback used: %s",
len(combined_results),
total_ms,
total_ms,
final_info.get("fallback_used", False),
)
......
......@@ -38,7 +38,7 @@ async def canifa_store_search(location: str) -> str:
# Search trên các cột structured: city, state, address, store_name
sql = f"""
SELECT store_name, address, city, state, phone_number,
SELECT store_name, address, city, state, phone_number,
schedule_name, time_open_today, time_close_today
FROM {STORE_TABLE}
WHERE LOWER(city) LIKE '%{clean}%'
......
......@@ -15,7 +15,7 @@ from config import (
logger = logging.getLogger(__name__)
DEFAULT_RESPONSE_TTL = 300
DEFAULT_RESPONSE_TTL = 0 # DISABLED — tắt cache response
RESPONSE_KEY_PREFIX = "resp_cache:"
EMBEDDING_CACHE_TTL = 86400 # 24 hours
......
This diff is collapsed.
#!/bin/bash
NUM_CORES=$(nproc)
WORKERS=$((2 * NUM_CORES + 1))
echo "🔧 [STARTUP] CPU cores: $NUM_CORES"
echo "🔧 [STARTUP] Gunicorn workers: $WORKERS"
exec gunicorn \
server:app \
--workers "$WORKERS" \
--worker-class uvicorn.workers.UvicornWorker \
--worker-connections 1000 \
--max-requests 1000 \
--max-requests-jitter 100 \
--timeout 30 \
--access-logfile - \
--error-logfile - \
--bind 0.0.0.0:5000 \
--log-level info
\ No newline at end of file
#!/bin/bash
NUM_CORES=$(nproc)
WORKERS=$((2 * NUM_CORES + 1))
echo "🔧 [STARTUP] CPU cores: $NUM_CORES"
echo "🔧 [STARTUP] Gunicorn workers: $WORKERS"
exec gunicorn \
server:app \
--workers "$WORKERS" \
--worker-class uvicorn.workers.UvicornWorker \
--worker-connections 1000 \
--max-requests 1000 \
--max-requests-jitter 100 \
--timeout 30 \
--access-logfile - \
--error-logfile - \
--bind 0.0.0.0:5000 \
--log-level info
......@@ -20,4 +20,3 @@ Get-NetTCPConnection -LocalPort 5000 | ForEach-Object { Stop-Process -Id $_.Owni
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
......@@ -819,6 +819,33 @@
let selectedToolPrompt = '';
let pendingImages = []; // 📸 Experimental: images to send with next message
// Color name → CSS hex (for color dot badge)
function getColorHex(colorName) {
if (!colorName) return '#888';
const c = colorName.toLowerCase();
const map = {
'đen': '#222', 'black': '#222',
'trắng': '#f5f5f5', 'white': '#f5f5f5',
'đỏ': '#e53935', 'red': '#e53935',
'xanh lá': '#43a047', 'green': '#43a047', 'xanh lá cây': '#43a047',
'xanh da trời': '#1e88e5', 'blue': '#1e88e5',
'xanh than': '#263238', 'aqua': '#00838f', 'navy': '#1a237e',
'xanh jeans': '#5c6bc0', 'jeans': '#5c6bc0',
'vàng': '#fdd835', 'yellow': '#fdd835',
'cam': '#fb8c00', 'orange': '#fb8c00',
'hồng': '#ec407a', 'pink': '#ec407a',
'tím': '#7b1fa2', 'purple': '#7b1fa2',
'nâu': '#6d4c41', 'brown': '#6d4c41',
'xám': '#9e9e9e', 'grey': '#9e9e9e', 'gray': '#9e9e9e',
'be': '#d7ccc8', 'beige': '#d7ccc8', 'kem': '#fff8e1',
'ghi': '#78909c',
};
for (const [key, hex] of Object.entries(map)) {
if (c.includes(key)) return hex;
}
return '#888';
}
// ==================== IMAGE HANDLING (Experimental) ====================
function handleImageSelect(event) {
const file = event.target.files[0];
......@@ -1564,9 +1591,17 @@
// SKU
const sku = document.createElement('div');
sku.className = 'product-sku';
sku.innerText = product.sku;
sku.innerText = product.sku_color || product.sku;
body.appendChild(sku);
// Color badge
if (product.color) {
const colorBadge = document.createElement('div');
colorBadge.style.cssText = 'font-size: 0.78em; color: #b0bec5; margin-bottom: 6px; display: flex; align-items: center; gap: 5px;';
colorBadge.innerHTML = `<span style="display:inline-block;width:10px;height:10px;border-radius:50%;border:1px solid #666;background:${getColorHex(product.color)};"></span> ${product.color}`;
body.appendChild(colorBadge);
}
// Name
const name = document.createElement('div');
name.className = 'product-name';
......
Xanh da trời/ Blue
Xanh lá cây/ Green
Màu xanh Jeans
Xanh than/ Aqua
\ No newline at end of file
import asyncio
import os
import sys
# Ensure backend is in path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common.starrocks_connection import get_db_connection
async def test_like():
sr = get_db_connection()
sql = """
SELECT master_color,
LOWER(master_color) LIKE '%xanh lá%' as b_like,
LOWER(master_color) LIKE '%xanh lá cây%' as b_like_cay
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE master_color LIKE '%xanh lá%' OR master_color LIKE '%Xanh lá%'
LIMIT 10
"""
rows = await sr.execute_query_async(sql)
for r in rows:
print(f"Color: '{r['master_color']}', LIKE '%xanh lá%': {r['b_like']}, LIKE '%xanh lá cây%': {r['b_like_cay']}")
print("-- Testing missing query --")
sql2 = """
SELECT COUNT(*) as c
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE LOWER(master_color) LIKE '%xanh lá%' OR LOWER(product_color_name) LIKE '%xanh lá%'
"""
rows2 = await sr.execute_query_async(sql2)
print(f"Total matched for '%xanh lá%': {rows2[0]['c']}")
if __name__ == "__main__":
asyncio.run(test_like())
import asyncio
import os
import sys
# Ensure backend directory is in path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from types import SimpleNamespace
from agent.tools.product_search_helpers import build_starrocks_query
from common.starrocks_connection import get_db_connection
async def main():
sr = get_db_connection()
print("=====================================================")
print("🧪 KỊCH BẢN 1: THEO ĐÚNG LOG LANGFUSE CON AI ĐÃ CHẠY:")
print(" product_name: 'Áo sơ mi xanh lá'")
print(" color: 'xanh lá'")
print("=====================================================")
params1 = SimpleNamespace(
description="product_name: Áo sơ mi xanh lá",
product_name="Áo sơ mi xanh lá",
gender_by_product=None,
age_by_product=None,
master_color="xanh lá",
price_min=None,
price_max=None,
discount_min=None,
discount_max=None,
magento_ref_code=None,
discovery_mode=None
)
# AI sẽ tự fetch Embedding thật 1536 chiều từ OpenAI (Bằng hàm create_embedding_async bên trong)
sql1, sql_params1 = await build_starrocks_query(params1)
# The output from build_starrocks_query is the exact SQL String and parameters
print("\n[SQL 1 - TỪ KHÓA TÊN SẢN PHẨM KHỚP SQL SẼ TẠO LÀ]:")
print(sql_params1)
# Run the query directly on DB
try:
rows1 = await sr.execute_query_async(sql1, sql_params1)
print(f"\n=> KẾT QUẢ KỊCH BẢN 1: Tìm thấy {len(rows1)} sản phẩm (MISS QUERY!).")
except Exception as e:
print(f"\n=> KẾT QUẢ KỊCH BẢN 1: Lỗi thực thi SQL: {e}")
print("\n\n=====================================================")
print("🧪 KỊCH BẢN 2: CHỮA BỆNH CHO CON AI (BỎ CHỮ 'xanh lá' RA KHỎI TÊN)")
print(" product_name: 'Áo sơ mi'")
print(" color: 'xanh lá'")
print("=====================================================")
params2 = SimpleNamespace(
description="product_name: Áo sơ mi xanh lá",
product_name="Áo sơ mi", # Đã bỏ chữ xanh lá ở đây
gender_by_product=None,
age_by_product=None,
master_color="xanh lá",
price_min=None,
price_max=None,
discount_min=None,
discount_max=None,
magento_ref_code=None,
discovery_mode=None
)
sql2, sql_params2 = await build_starrocks_query(params2)
print("\n[SQL 2 - TỪ KHÓA TÊN SẢN PHẨM THỰC TẾ TRÚNG LÚC NÀY]:")
print(sql_params2)
try:
rows2 = await sr.execute_query_async(sql2, sql_params2)
print(f"\n=> KẾT QUẢ KỊCH BẢN 2: Tìm thấy {len(rows2)} sản phẩm (MATCH NGON Ơ!).")
for i, row in enumerate(rows2[:3]):
print(f" [{i+1}]. Sản phẩm: '{row.get('product_name')}' - MÀU: '{row.get('master_color')}'")
if len(rows2) > 3:
print(" ...")
except Exception as e:
print(f"Lỗi: {e}")
if __name__ == "__main__":
asyncio.run(main())
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