Commit 0205042f authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

fix: cross-search ao lot/ao bra, remove product_line_vn, add cross-sell + gender ultimatum

parent bd608ad5
...@@ -163,9 +163,19 @@ Trong DB, cột `description_text` BẮT ĐẦU bằng tên sản phẩm: ...@@ -163,9 +163,19 @@ Trong DB, cột `description_text` BẮT ĐẦU bằng tên sản phẩm:
- "quần jeans" → `description: "Quần jean"`, `product_name: "Quần jean"` - "quần jeans" → `description: "Quần jean"`, `product_name: "Quần jean"`
- "áo polo" → `description: "Áo Polo"`, `product_name: "Áo Polo"` - "áo polo" → `description: "Áo Polo"`, `product_name: "Áo Polo"`
- "váy jeans" → `description: "Váy jeans"`, `product_name: "Váy jeans"` - "váy jeans" → `description: "Váy jeans"`, `product_name: "Váy jeans"`
- **"áo bra"** → `description: "Áo bra active"`, `product_name: "Áo bra active"` (KHÔNG PHẢI "Áo lót"!)
→ Vì semantic search sẽ match trực tiếp với đầu description_text trong DB! → Vì semantic search sẽ match trực tiếp với đầu description_text trong DB!
**→ NHÓM SP LIÊN QUAN (description bao cả 2 để vector search tìm hết):**
- **"áo bra"** / **"áo lót"** → `description: "Áo lót. Áo bra active thoáng mát co giãn"`, `product_name: "Áo lót/Áo bra active"`
→ description ghi cả 2 tên để semantic search tìm được cả 2 loại
→ product_name dùng "/" để code tự tách và lọc cả 2
**⛔⛔⛔ TỐI HẬU THƯ — CẤM TỰ SUY DIỄN gender/age ⛔⛔⛔**
User nói "áo lót" → gender: null, age: null (**CẤM tự điền "women"!**)
User nói "áo phông" → gender: null, age: null
→ KHÔNG BIẾT thì KHÔNG ĐIỀN! Để null = tìm TẤT CẢ. Điền sai = MẤT kết quả!
→ CHỈ điền khi user NÓI RÕ: "áo lót nữ" → women, "áo trẻ em" → kid
**→ Khi khách hỏi MÔ TẢ / NHU CẦU (HYDE):** `description` ≠ `product_name` **→ Khi khách hỏi MÔ TẢ / NHU CẦU (HYDE):** `description` ≠ `product_name`
- "đi dự tiệc" → `description: "Váy dự tiệc sang trọng nữ tính"`, `product_name: "Váy liền"` - "đi dự tiệc" → `description: "Váy dự tiệc sang trọng nữ tính"`, `product_name: "Váy liền"`
- "đồ đi biển" → `description: "Áo phông nam đi biển thoáng mát"`, `product_name: "Áo phông"` - "đồ đi biển" → `description: "Áo phông nam đi biển thoáng mát"`, `product_name: "Áo phông"`
...@@ -205,6 +215,13 @@ description = "áo len giá dưới 500k" # ← SAI - có giá trong ...@@ -205,6 +215,13 @@ description = "áo len giá dưới 500k" # ← SAI - có giá trong
description = "Váy liền thân/ Chân váy" # ← SAI khi hỏi "váy jeans" - phải ghi "Váy jeans" description = "Váy liền thân/ Chân váy" # ← SAI khi hỏi "váy jeans" - phải ghi "Váy jeans"
``` ```
**⚡ 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!
--- ---
### 5.3. TỰ SUY LUẬN KHI THIẾU THÔNG TIN ### 5.3. TỰ SUY LUẬN KHI THIẾU THÔNG TIN
...@@ -452,6 +469,7 @@ Khách: "Mẫu này có size gì?" ← KHÔNG có chữ "CÒN" → Liệt kê t ...@@ -452,6 +469,7 @@ Khách: "Mẫu này có size gì?" ← KHÔNG có chữ "CÒN" → Liệt kê t
- Đọc trường `size_scale` từ tool response → liệt kê TỪNG SIZE cụ thể - Đọc trường `size_scale` từ tool response → liệt kê TỪNG SIZE cụ thể
- KHÔNG dùng từ "đủ", "đầy đủ", "từ...đến..." - KHÔNG dùng từ "đủ", "đầy đủ", "từ...đến..."
- KHÔNG suy diễn size — chỉ nói size có trong data - KHÔNG suy diễn size — chỉ nói size có trong data
- ⛔ **CẤM BỊA SIZE!** Nếu size_scale = "32A|34A|36A|38A|40A" → chỉ nói 32A, 34A, 36A, 38A, 40A. TUYỆT ĐỐI KHÔNG tự thêm S, M, L, XL vào!
--- ---
......
...@@ -22,5 +22,12 @@ with open(os.path.join(BASE, "05_tool_routing.txt"), "r", encoding="utf-8") as f ...@@ -22,5 +22,12 @@ with open(os.path.join(BASE, "05_tool_routing.txt"), "r", encoding="utf-8") as f
lf.create_prompt(name="canifa-05-tool-routing", prompt=content2, labels=["production"], tags=["canifa", "system-core"], type="text") lf.create_prompt(name="canifa-05-tool-routing", prompt=content2, labels=["production"], tags=["canifa", "system-core"], type="text")
print(f"✅ canifa-05-tool-routing updated ({len(content2):,} chars)") print(f"✅ canifa-05-tool-routing updated ({len(content2):,} chars)")
# 3. Push data_retrieval_tool prompt
drt_path = os.path.join(BASE, "..", "tool_prompts", "data_retrieval_tool.txt")
with open(drt_path, "r", encoding="utf-8") as f:
content3 = f.read()
lf.create_prompt(name="data_retrieval_tool", prompt=content3, labels=["production"], tags=["canifa", "tool-prompt"], type="text")
print(f"✅ data_retrieval_tool updated ({len(content3):,} chars)")
lf.flush() lf.flush()
print("Done!") print("Done!")
...@@ -134,6 +134,26 @@ form_neckline — Crew Neck, Classic Collar, V-neck, Hooded collar, Mock Neck/ H ...@@ -134,6 +134,26 @@ form_neckline — Crew Neck, Classic Collar, V-neck, Hooded collar, Mock Neck/ H
material_group — Knit - Dệt Kim, Woven - Dệt Thoi, Yarn - Sợi material_group — Knit - Dệt Kim, Woven - Dệt Thoi, Yarn - Sợi
season — Fall Winter, Spring Summer, Year 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Õ!
User nói "áo lót" → gender: null, age: null (CẤM tự điền "women"!)
User nói "áo bra" → gender: null, age: null (CẤM tự điền "women"!)
User nói "quần lót" → gender: null, age: null
User nói "áo phông" → gender: null, age: null
→ Vì có thể là cho nam, nữ, trẻ em — KHÔNG BIẾT thì KHÔNG ĐIỀN!
CHỈ ĐIỀN KHI USER NÓI RÕ:
- "áo lót nữ" → gender: "women"
- "áo lót trẻ em" → age: "kid"
- "áo phông nam" → gender: "men"
- "quần cho bé trai" → gender: "boy", age: "kid"
⚠️ GHI NHỚ: Để null = tìm TẤT CẢ (nam + nữ + trẻ em). Điền sai = MẤT kết quả!
═══════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════
📝 VÍ DỤ (description_text LUÔN CÓ!) 📝 VÍ DỤ (description_text LUÔN CÓ!)
═══════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════
...@@ -185,6 +205,12 @@ CASE 10: "Áo khaki" ...@@ -185,6 +205,12 @@ CASE 10: "Áo khaki"
→ description: "product_name: Áo khaki. description_text: Áo chất liệu khaki form đẹp" → description: "product_name: Áo khaki. description_text: Áo chất liệu khaki form đẹp"
→ product_line_vn: "Áo" → product_line_vn: "Áo"
CASE 11: "Áo lót" hoặc "Áo bra" (NHÓM SP LIÊN QUAN)
→ description: "product_name: Áo lót/Áo bra active. description_text: Áo lót. Áo bra active thoáng mát co giãn tốt"
→ product_name: "Áo lót/Áo bra active"
⚠️ KHÔNG tự suy gender/age! User nói "áo lót" chung → để null. Chỉ điền khi user NÓI RÕ (VD: "áo lót nữ" → women, "áo lót trẻ em" → kid)
⚠️ description_text PHẢI ghi CẢ 2 tên (Áo lót + Áo bra active) để semantic search tìm được cả 2 loại!
═══════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════
🎉 DỊP LỄ / SỰ KIỆN — description_text ghi lý do + gợi ý phong cách 🎉 DỊP LỄ / SỰ KIỆN — description_text ghi lý do + gợi ý phong cách
═══════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════
......
...@@ -84,16 +84,6 @@ class SearchItem(BaseModel): ...@@ -84,16 +84,6 @@ class SearchItem(BaseModel):
discount_max: int | None = Field( discount_max: int | None = Field(
description="[SQL FILTER] % giảm giá tối đa. VD: 70 → chỉ lấy SP giảm <= 70%.", description="[SQL FILTER] % giảm giá tối đa. VD: 70 → chỉ lấy SP giảm <= 70%.",
) )
product_line_vn: str | None = Field(
description=(
"[SQL FILTER] Đại loại sản phẩm — lọc RỘNG bằng prefix. "
"Hỗ trợ nhiều giá trị cách bằng '/'. "
"GIÁ TRỊ: 'Áo' (mọi loại áo), 'Quần' (mọi loại quần), "
"'Váy' (váy liền + chân váy), 'Bộ' (bộ quần áo, bộ mặc nhà), "
"'Khăn', 'Mũ', 'Túi', 'Tất', 'Chăn'. "
"VD: 'áo khaki' → product_line_vn: 'Áo'. 'quần hoặc váy' → 'Quần/ Váy'"
),
)
discovery_mode: str | None = Field( discovery_mode: str | None = Field(
description=( description=(
"[SQL FILTER] Chế độ khám phá. 'new' = hàng mới (is_new_product=1), " "[SQL FILTER] Chế độ khám phá. 'new' = hàng mới (is_new_product=1), "
...@@ -182,7 +172,6 @@ async def _execute_single_search( ...@@ -182,7 +172,6 @@ async def _execute_single_search(
"age": item.age_by_product, "age": item.age_by_product,
"color": item.master_color, "color": item.master_color,
"price_range": f"{item.price_min or ''}-{item.price_max or ''}", "price_range": f"{item.price_min or ''}-{item.price_max or ''}",
"product_line_vn": item.product_line_vn,
"magento_ref_code": item.magento_ref_code, "magento_ref_code": item.magento_ref_code,
"discovery_mode": item.discovery_mode, "discovery_mode": item.discovery_mode,
}, },
...@@ -224,7 +213,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str: ...@@ -224,7 +213,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
for i, s in enumerate(searches): for i, s in enumerate(searches):
logger.info( logger.info(
"🔧 Search[%d]: desc=%r, name=%r, gender=%s, age=%s, color=%s, " "🔧 Search[%d]: desc=%r, name=%r, gender=%s, age=%s, color=%s, "
"price=%s-%s, line=%s, code=%s, mode=%s", "price=%s-%s, code=%s, mode=%s",
i, i,
(s.description[:80] + "...") if s.description and len(s.description) > 80 else s.description, (s.description[:80] + "...") if s.description and len(s.description) > 80 else s.description,
s.product_name, s.product_name,
...@@ -232,7 +221,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str: ...@@ -232,7 +221,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
s.age_by_product, s.age_by_product,
s.master_color, s.master_color,
s.price_min, s.price_max, s.price_min, s.price_max,
s.product_line_vn,
s.magento_ref_code, s.magento_ref_code,
s.discovery_mode, s.discovery_mode,
) )
...@@ -263,7 +251,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str: ...@@ -263,7 +251,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
search_inputs = [ search_inputs = [
{ {
"description": item.description, "description": item.description,
"product_line_vn": item.product_line_vn,
"gender_by_product": item.gender_by_product, "gender_by_product": item.gender_by_product,
"age_by_product": item.age_by_product, "age_by_product": item.age_by_product,
"discovery_mode": item.discovery_mode, "discovery_mode": item.discovery_mode,
......
...@@ -12,7 +12,7 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = { ...@@ -12,7 +12,7 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo nỉ có mũ": ["áo nỉ có mũ"], "Áo nỉ có mũ": ["áo nỉ có mũ"],
"Áo nỉ": ["áo nỉ"], "Áo nỉ": ["áo nỉ"],
"Áo mặc nhà": ["áo mặc nhà"], "Áo mặc nhà": ["áo mặc nhà"],
"Áo lót": ["áo lót", "áo ngực", "áo quây"], "Áo lót": ["áo lót", "áo ngực", "áo quây", "áo lót nữ", "áo lót nam", "áo lót trẻ em"],
"Áo len gilet": ["áo len gilet"], "Áo len gilet": ["áo len gilet"],
"Áo len": ["áo len"], "Áo len": ["áo len"],
"Áo kiểu": ["áo kiểu"], "Áo kiểu": ["áo kiểu"],
...@@ -76,6 +76,19 @@ for db_value, synonyms in PRODUCT_LINE_MAP.items(): ...@@ -76,6 +76,19 @@ for db_value, synonyms in PRODUCT_LINE_MAP.items():
for syn in synonyms: for syn in synonyms:
SYNONYM_TO_DB[syn.lower()] = db_value SYNONYM_TO_DB[syn.lower()] = db_value
# ==============================================================================
# RELATED LINES: hỏi "áo bra" → tìm cả "Áo bra active" + "Áo lót" và ngược lại
# ==============================================================================
RELATED_LINES: dict[str, list[str]] = {
"Áo bra active": ["Áo lót"],
"Áo lót": ["Áo bra active"],
}
def get_related_lines(product_line: str) -> list[str]:
"""VD: get_related_lines("Áo bra active") → ["Áo bra active", "Áo lót"]"""
return [product_line] + RELATED_LINES.get(product_line, [])
# Pre-sort synonyms by length DESC for longest-match-first # Pre-sort synonyms by length DESC for longest-match-first
_SORTED_SYNONYMS = sorted(SYNONYM_TO_DB.keys(), key=len, reverse=True) _SORTED_SYNONYMS = sorted(SYNONYM_TO_DB.keys(), key=len, reverse=True)
......
...@@ -87,18 +87,28 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]: ...@@ -87,18 +87,28 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]:
GENERIC_WORDS = {key.split()[0].lower() for key in PRODUCT_LINE_MAP.keys()} GENERIC_WORDS = {key.split()[0].lower() for key in PRODUCT_LINE_MAP.keys()}
name_val = getattr(params, "product_name", None) name_val = getattr(params, "product_name", None)
if name_val: if name_val:
from agent.tools.product_mapping import resolve_product_name from agent.tools.product_mapping import resolve_product_name, get_related_lines
resolved_name = resolve_product_name(name_val)
words = resolved_name.lower().strip().split() # Support '/' separator: "Áo lót/Áo bra active" → ["Áo lót", "Áo bra active"]
keywords = [w for w in words if w not in GENERIC_WORDS and len(w) >= 2] name_parts = [p.strip() for p in name_val.split("/") if p.strip()]
# Fallback: nếu tất cả đều generic (VD: "quần"), vẫn dùng làm filter
if not keywords: all_phrases = set()
keywords = [w for w in words if len(w) >= 2] for part in name_parts:
for kw in keywords: resolved = resolve_product_name(part)
clauses.append("LOWER(product_name) LIKE %s") # Also expand related lines
sql_params.append(f"%{kw}%") for rname in get_related_lines(resolved):
if keywords: words = rname.strip().split()
logger.info(f"🏷️ [SQL FILTER] Product name: '{name_val}' → resolved: '{resolved_name}' → keywords: {keywords}") phrase = " ".join(w for w in words if w.lower() not in GENERIC_WORDS)
if phrase:
all_phrases.add(phrase.lower())
if all_phrases:
like_parts = []
for phrase in all_phrases:
like_parts.append("LOWER(product_name) LIKE %s")
sql_params.append(f"%{phrase}%")
clauses.append(f"({' OR '.join(like_parts)})")
logger.info(f"🏷️ [SQL FILTER] Product name: '{name_val}' → phrases: {all_phrases}")
return clauses return clauses
......
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