Commit 4f52d746 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat(rules): seed 350+ fashion rules with full demographic & anchor coverage, add tests/

parent a2fe4bd9
# D:\cnf\chatbot-canifa-feedback\.agent\workflows\fashion-rules-verification.md
# Fashion Rules Verification Loop
## Mục đích
Tự động hóa việc kiểm thử hàng loạt (batch testing) các luật phối đồ (fashion rules) trên danh mục sản phẩm. Đảm bảo rằng Engine recommend các sản phẩm chính xác theo Giới tính (Gender), Dịp mặc (Occasion) và Luật kết hợp (Pairing Rules) đã được cấu hình trong database.
## Quy trình (Workflow)
**Mục tiêu:** Kiểm tra N sản phẩm (ví dụ: 100) để xác minh:
1. Sản phẩm có nhận diện đúng Giới tính và Phân loại (Product Line) không.
2. Kết quả recommend có chứa các Dịp mặc (Occasion) được phép theo luật hay không.
3. Các sản phẩm gợi ý (Target Items) có thuộc đúng Product Line quy định trong `chatbot_fashion_rules` hay không.
### Bước 1: Viết script kiểm thử tự động
Bạn có thể tự động sinh ra một file script bằng Python để fetch catalog trực tiếp thông qua `StylistEngine` và test:
- **Input:** Lấy ngẫu nhiên X sản phẩm từ catalog, đảm bảo có đủ giới tính (Nữ, Nam, Unisex, Bé Gái, Bé Trai).
- **Process:** Khởi tạo `StylistEngine`, gọi `compute_dynamic_rule_matches(code)`.
- **Verify:**
- Truy vấn luật trực tiếp từ hàm `_fetch_rules_with_reason(anchor_cat, gender)`.
- Đối chiếu kết quả recommend từ engine so với tập luật này.
- Ghi nhận: Dịp nào thiếu SP recommend (do catalog không có màu phù hợp), item nào bị recommend sai category (Vi phạm luật).
- **Output:** In ra báo cáo tóm tắt (Tổng số, Số SP pass, Số lượng lỗi if any).
### Bước 2: Chạy script và nhận report
// turbo
```bash
cd backend
python test_fashion_rules_batch.py --limit 100
```
### Bước 3: Đánh giá và Sửa lỗi
- Nếu phát hiện Occasion rỗng, nguyên nhân có thể do catalog thiếu sản phẩm có màu sắc phù hợp quy tắc phối, hoặc luật quá khắt khe.
- Nếu phát hiện Item recommend sai `product_line`, cần check lại hàm query rule của Engine hoặc việc fallback.
- Chỉnh sửa logic trong `stylist_engine.py` hoặc thêm/sửa rules trong bảng `chatbot_fashion_rules`. Lặp lại Bước 2.
"""
migrate_003_full_coverage.py
────────────────────────────────────────────────────────────
Seed toàn bộ anchor categories còn thiếu rules:
- Bottom items as anchor (Quần jean, Quần soóc, Chân váy, ...)
- Outerwear as anchor (Áo nỉ, Áo khoác, Áo len, ...)
- Kids sets & home wear (Bộ mặc nhà, Áo mặc nhà, ...)
Run:
cd backend
.venv\Scripts\python.exe database/migrate/migrate_003_full_coverage.py
"""
import logging
import os
import sys
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
TABLE = "dashboard_canifa.chatbot_fashion_rules"
# ──────────────────────────────────────────────────────────────────────────────
# RULES — (gender_target, anchor_category, occasion_tag, match_role, target_category, ai_reason)
# ──────────────────────────────────────────────────────────────────────────────
RULES = [
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần jean
# ══════════════════════════════════════════════════════════════════
("nu", "Quần jean", "di_choi", "top", "Áo phông", "Quần jean + Áo phông: combo quốc dân nữ"),
("nu", "Quần jean", "di_choi", "top", "Áo kiểu", "Quần jean + Áo kiểu: trendy dạo phố"),
("nu", "Quần jean", "di_choi", "outerwear","Áo khoác gió","Quần jean + Áo khoác gió: street layer"),
("nu", "Quần jean", "hang_ngay","top", "Áo phông", "Quần jean + Áo phông: casual everyday"),
("nu", "Quần jean", "du_lich", "top", "Áo phông", "Quần jean + Áo phông: du lịch thoải mái"),
("nam", "Quần jean", "di_choi", "top", "Áo phông", "Quần jean + Áo phông: timeless nam"),
("nam", "Quần jean", "di_choi", "top", "Áo nỉ", "Quần jean + Áo nỉ: cold weather casual"),
("nam", "Quần jean", "di_choi", "outerwear","Áo khoác gió","Quần jean + Áo khoác gió: street layer"),
("nam", "Quần jean", "hang_ngay","top", "Áo phông", "Quần jean + Áo phông: everyday easy"),
("unisex", "Quần jean", "hang_ngay","top", "Áo phông", "Quần jean + Áo phông: universal"),
("unisex", "Quần jean", "hang_ngay","top", "Áo Sơ mi", "Quần jean + Sơ mi oversize: effortless"),
("unisex", "Quần jean", "di_choi", "top", "Áo nỉ có mũ","Quần jean + Hoodie: street essential"),
("be_gai", "Quần jean", "hang_ngay","top", "Áo phông", "Quần jean + Áo phông bé gái"),
("be_trai","Quần jean", "hang_ngay","top", "Áo phông", "Quần jean + Áo phông bé trai"),
("be_trai","Quần jean", "du_lich", "top", "Áo phông", "Quần jean du lịch bé trai"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần soóc
# ══════════════════════════════════════════════════════════════════
("nu", "Quần soóc", "di_choi", "top", "Áo phông", "Quần soóc + Áo phông: hè năng động nữ"),
("nu", "Quần soóc", "di_choi", "top", "Áo kiểu", "Quần soóc + Áo kiểu: hot girl combo"),
("nu", "Quần soóc", "du_lich", "top", "Áo hai dây","Quần soóc + Áo 2 dây: biển mùa hè"),
("nu", "Quần soóc", "du_lich", "top", "Áo phông", "Quần soóc + Áo phông: resort casual"),
("nam", "Quần soóc", "di_choi", "top", "Áo phông", "Quần soóc + Áo phông: chill hè nam"),
("nam", "Quần soóc", "du_lich", "top", "Áo phông", "Quần soóc + Áo phông: beach ready"),
("nam", "Quần soóc", "du_lich", "top", "Áo Polo", "Quần soóc + Áo Polo: resort lịch sự"),
("be_trai","Quần soóc", "hang_ngay","top", "Áo phông", "Quần soóc + Áo phông: bé trai hàng ngày"),
("be_gai", "Quần soóc", "di_choi", "top", "Áo phông", "Quần soóc + Áo phông bé gái đi chơi"),
("unisex", "Quần soóc", "the_thao", "top", "Áo phông", "Quần soóc + Áo phông: workout"),
# ══════════════════════════════════════════════════════════════════
# 👗 BOTTOM AS ANCHOR — Chân váy
# ══════════════════════════════════════════════════════════════════
("nu", "Chân váy", "di_lam", "top", "Blouse", "Chân váy + Blouse: feminine office"),
("nu", "Chân váy", "di_lam", "top", "Áo Sơ mi", "Chân váy + Sơ mi: clean professional"),
("nu", "Chân váy", "di_lam", "outerwear","Blazer", "Chân váy + Blazer: power look"),
("nu", "Chân váy", "di_choi", "top", "Áo phông", "Chân váy + Áo phông: casual feminine"),
("nu", "Chân váy", "di_choi", "top", "Áo kiểu", "Chân váy + Áo kiểu: chic dạo phố"),
("nu", "Chân váy", "du_lich", "top", "Áo phông", "Chân váy + Áo phông: vacation style"),
("be_gai","Chân váy", "di_choi", "top", "Áo phông", "Chân váy + Áo phông: bé gái dễ thương"),
("be_gai","Chân váy", "di_choi", "top", "Áo kiểu", "Chân váy + Áo kiểu: bé gái xinh đẹp"),
# ══════════════════════════════════════════════════════════════════
# 👗 STANDALONE/OTR ANCHOR — Váy liền
# ══════════════════════════════════════════════════════════════════
("nu", "Váy liền", "di_choi", "outerwear","Áo khoác gió","Váy liền + Áo khoác: airy street style"),
("nu", "Váy liền", "du_lich", "outerwear","Áo khoác gió","Váy liền + Áo khoác: resort complete"),
("nu", "Váy liền", "di_lam", "outerwear","Blazer", "Váy liền + Blazer: professional smart"),
("nu", "Váy liền", "di_lam", "outerwear","Cardigan", "Váy liền + Cardigan: feminine office"),
("be_gai","Váy liền", "di_choi", "outerwear","Áo khoác gió","Váy liền + Áo khoác: bé gái đi chơi"),
("be_gai","Váy liền", "du_lich", "outerwear","Áo khoác gió","Váy liền lanh + khoác: du lịch bé gái"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần mặc nhà / Quần đùi cotton
# ══════════════════════════════════════════════════════════════════
("nu", "Quần mặc nhà", "mac_nha", "top", "Áo phông", "Quần mặc nhà + Áo phông: bộ nhà nữ"),
("nu", "Quần mặc nhà", "mac_nha", "top", "Áo hai dây", "Quần mặc nhà + Áo 2 dây: lounge nhẹ"),
("nu", "Quần mặc nhà", "mac_nha", "top", "Áo mặc nhà", "Quần mặc nhà + Áo mặc nhà: bộ đủ bộ"),
("nam", "Quần mặc nhà", "mac_nha", "top", "Áo phông", "Quần mặc nhà + Áo phông: bộ nhà nam"),
("be_trai","Quần mặc nhà", "mac_nha", "top", "Áo phông", "Quần mặc nhà + Áo phông bé trai"),
("be_gai", "Quần mặc nhà", "mac_nha", "top", "Áo phông", "Quần mặc nhà + Áo phông bé gái"),
("all", "Quần đùi cotton","mac_nha", "top", "Áo phông", "Quần đùi + Áo phông: chill home basic"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần leggings
# ══════════════════════════════════════════════════════════════════
("nu", "Quần leggings", "the_thao", "top", "Áo phông", "Leggings + Áo phông: workout nữ"),
("nu", "Quần leggings", "hang_ngay","top", "Áo phông", "Leggings + Áo phông: athleisure"),
("be_gai", "Quần leggings", "hang_ngay","top", "Áo phông", "Leggings + Áo phông bé gái"),
("be_gai", "Quần leggings", "hang_ngay","top", "Áo kiểu", "Leggings + Áo kiểu bé gái cute"),
("unisex", "Quần leggings", "the_thao", "top", "Áo phông", "Leggings + Áo phông: universal sport"),
("be_trai","Quần leggings", "the_thao", "top", "Áo phông", "Leggings + Áo phông: bé trai vận động"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần dài (casual)
# ══════════════════════════════════════════════════════════════════
("nu", "Quần dài", "hang_ngay", "top", "Áo phông", "Quần dài + Áo phông: everyday comfort"),
("nu", "Quần dài", "du_lich", "top", "Áo phông", "Quần dài + Áo phông: travel comfort"),
("nam", "Quần dài", "hang_ngay", "top", "Áo phông", "Quần dài + Áo phông: easy casual"),
("be_trai","Quần dài", "du_lich", "top", "Áo phông", "Quần dài + Áo phông: dã ngoại bé trai"),
("unisex", "Quần dài", "hang_ngay", "top", "Áo phông", "Quần dài + Áo phông: universal"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần thể thao
# ══════════════════════════════════════════════════════════════════
("nu", "Quần thể thao", "the_thao", "top", "Áo phông", "Quần thể thao + Áo phông: gym nữ"),
("nam", "Quần thể thao", "the_thao", "top", "Áo phông", "Quần thể thao + Áo phông: gym nam"),
("be_trai","Quần thể thao", "the_thao", "top", "Áo phông", "Quần thể thao + Áo phông: bé trai"),
("be_gai", "Quần thể thao", "the_thao", "top", "Áo phông", "Quần thể thao + Áo phông: bé gái"),
("unisex", "Quần thể thao", "the_thao", "top", "Áo phông", "Quần thể thao + Áo phông: sport"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần âu & Quần khaki
# ══════════════════════════════════════════════════════════════════
("nu", "Quần âu", "di_lam", "top", "Blouse", "Quần âu + Blouse: professional nữ"),
("nu", "Quần âu", "di_lam", "top", "Áo Sơ mi", "Quần âu + Sơ mi: classic office nữ"),
("nu", "Quần âu", "di_lam", "top", "Áo kiểu", "Quần âu + Áo kiểu: feminine office"),
("nam", "Quần âu", "di_lam", "top", "Áo Sơ mi", "Quần âu + Sơ mi: business classic"),
("nam", "Quần âu", "di_lam", "top", "Áo Polo", "Quần âu + Áo Polo: neat casual"),
("nam", "Quần khaki","di_lam", "top", "Áo Sơ mi", "Quần khaki + Sơ mi: smart casual"),
("nam", "Quần khaki","di_lam", "top", "Áo Polo", "Quần khaki + Áo Polo: business casual"),
("nam", "Quần khaki","du_lich", "top", "Áo phông", "Quần khaki + Áo phông: adventure ready"),
("be_trai","Quần khaki","hang_ngay","top","Áo phông", "Quần khaki + Áo phông bé trai"),
("be_trai","Quần khaki","du_lich", "top","Áo phông", "Quần khaki + Áo phông: dã ngoại"),
# ══════════════════════════════════════════════════════════════════
# 👖 BOTTOM AS ANCHOR — Quần nỉ
# ══════════════════════════════════════════════════════════════════
("be_trai","Quần nỉ", "hang_ngay","top", "Áo phông", "Quần nỉ + Áo phông: ấm ngày lạnh bé trai"),
("be_trai","Quần nỉ", "hang_ngay","top", "Áo nỉ", "Quần nỉ + Áo nỉ: bộ nỉ bé trai"),
("be_gai", "Quần nỉ", "hang_ngay","top", "Áo phông", "Quần nỉ + Áo phông: bé gái ấm áp"),
("be_gai", "Quần nỉ", "hang_ngay","top", "Áo kiểu", "Quần nỉ + Áo kiểu: comfortable cute"),
("unisex", "Quần nỉ", "hang_ngay","top", "Áo phông", "Quần nỉ + Áo phông: cozy basics"),
("nu", "Quần nỉ", "mac_nha", "top", "Áo phông", "Quần nỉ + Áo phông: home warmth"),
("nam", "Quần nỉ", "mac_nha", "top", "Áo phông", "Quần nỉ + Áo phông nam: home comfort"),
# ══════════════════════════════════════════════════════════════════
# 🧥 OUTERWEAR AS ANCHOR — Áo nỉ (sweater/sweatshirt)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo nỉ", "di_choi", "bottom","Quần jean", "Áo nỉ + Quần jean: casual mùa lạnh nữ"),
("nu", "Áo nỉ", "hang_ngay","bottom","Quần jean", "Áo nỉ + Quần jean: everyday nữ"),
("nu", "Áo nỉ", "mac_nha", "bottom","Quần mặc nhà","Áo nỉ + Quần mặc nhà: cozy home nữ"),
("be_gai", "Áo nỉ", "hang_ngay","bottom","Quần leggings","Áo nỉ + Leggings: bé gái ấm áp"),
("be_gai", "Áo nỉ", "hang_ngay","bottom","Quần nỉ", "Áo nỉ + Quần nỉ: bộ nỉ bé gái"),
# ══════════════════════════════════════════════════════════════════
# 🧥 OUTERWEAR AS ANCHOR — Áo nỉ có mũ (hoodie)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo nỉ có mũ", "di_choi", "bottom","Quần jean", "Hoodie + Quần jean: nữ street style"),
("nu", "Áo nỉ có mũ", "hang_ngay","bottom","Quần jean", "Hoodie + Quần jean nữ hàng ngày"),
("nam", "Áo nỉ có mũ", "di_choi", "bottom","Quần jean", "Hoodie + Quần jean: nam street classic"),
("be_trai","Áo nỉ có mũ", "hang_ngay","bottom","Quần nỉ", "Hoodie + Quần nỉ: ấm ngày lạnh bé trai"),
("be_trai","Áo nỉ có mũ", "hang_ngay","bottom","Quần dài", "Hoodie + Quần dài: bé trai casual"),
("be_trai","Áo nỉ có mũ", "di_choi", "bottom","Quần jean", "Hoodie + Quần jean: bé trai đi chơi"),
("unisex", "Áo nỉ có mũ", "di_choi", "bottom","Quần jean", "Hoodie + Quần jean: unisex street"),
# ══════════════════════════════════════════════════════════════════
# 🧥 OUTERWEAR AS ANCHOR — Áo khoác dáng ngắn
# ══════════════════════════════════════════════════════════════════
("nu", "Áo khoác dáng ngắn", "di_choi", "bottom","Quần jean", "Khoác ngắn + Quần jean: trendy nữ"),
("nu", "Áo khoác dáng ngắn", "di_choi", "top", "Áo phông", "Khoác ngắn phủ Áo phông nữ"),
("nu", "Áo khoác dáng ngắn", "du_lich", "bottom","Quần jean", "Khoác ngắn + Quần jean du lịch"),
("nam", "Áo khoác dáng ngắn", "di_choi", "bottom","Quần jean", "Khoác ngắn + Quần jean nam"),
("nam", "Áo khoác dáng ngắn", "di_choi", "top", "Áo phông", "Khoác ngắn phủ Áo phông nam"),
("unisex", "Áo khoác dáng ngắn", "di_choi", "bottom","Quần jean", "Khoác ngắn + Quần jean: unisex"),
("unisex", "Áo khoác dáng ngắn", "di_choi", "top", "Áo phông", "Khoác ngắn + Áo phông: layer"),
("be_gai", "Áo khoác dáng ngắn", "di_choi", "bottom","Chân váy", "Khoác ngắn + Chân váy: bé gái cute"),
("be_gai", "Áo khoác dáng ngắn", "hang_ngay","bottom","Quần leggings","Khoác ngắn + Leggings bé gái"),
("be_trai","Áo khoác dáng ngắn", "hang_ngay","bottom","Quần jean", "Khoác ngắn + Quần jean bé trai"),
# ══════════════════════════════════════════════════════════════════
# 🧥 OUTERWEAR AS ANCHOR — Áo khoác chống nắng
# ══════════════════════════════════════════════════════════════════
("nu", "Áo khoác chống nắng", "du_lich", "top", "Áo phông", "Khoác chống nắng + Áo phông nữ"),
("nu", "Áo khoác chống nắng", "du_lich", "bottom","Quần soóc", "Khoác chống nắng + Quần soóc biển"),
("nam", "Áo khoác chống nắng", "du_lich", "top", "Áo phông", "Khoác chống nắng + Áo phông nam"),
("unisex","Áo khoác chống nắng", "du_lich", "top", "Áo phông", "Khoác chống nắng: universal travel"),
("unisex","Áo khoác chống nắng", "the_thao", "top", "Áo phông", "Khoác chống nắng: sport outdoor"),
# ══════════════════════════════════════════════════════════════════
# 🧥 OUTERWEAR AS ANCHOR — Áo len
# ══════════════════════════════════════════════════════════════════
("nu", "Áo len", "hang_ngay","bottom","Quần jean", "Áo len + Quần jean: cozy casual nữ"),
("nu", "Áo len", "di_lam", "bottom","Quần âu", "Áo len + Quần âu: knit office style"),
("nu", "Áo len", "di_choi", "bottom","Quần jean", "Áo len + Quần jean: autumn casual"),
("nam", "Áo len", "hang_ngay","bottom","Quần jean", "Áo len + Quần jean nam: relaxed weekend"),
("nam", "Áo len", "di_lam", "bottom","Quần khaki", "Áo len + Quần khaki: smart casual"),
("be_gai","Áo len", "hang_ngay","bottom","Quần leggings","Áo len + Leggings: bé gái ấm áp"),
("be_gai","Áo len", "hang_ngay","bottom","Quần nỉ", "Áo len + Quần nỉ: cozy bé gái"),
# ══════════════════════════════════════════════════════════════════
# 🧥 OUTERWEAR AS ANCHOR — Áo len gilet (vest)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo len gilet", "di_lam", "top", "Áo Sơ mi", "Gilet len + Sơ mi: layering công sở"),
("nu", "Áo len gilet", "hang_ngay","top", "Áo phông", "Gilet len + Áo phông: casual layer"),
("nam", "Áo len gilet", "di_lam", "top", "Áo Sơ mi", "Gilet len + Sơ mi: preppy style nam"),
("be_trai","Áo len gilet", "hang_ngay","top", "Áo phông", "Gilet len + Áo phông: bé trai ấm"),
("be_trai","Áo len gilet", "hang_ngay","bottom","Quần nỉ", "Gilet len + Quần nỉ: bé trai đông"),
# ══════════════════════════════════════════════════════════════════
# 🏠 HOME WEAR AS ANCHOR — Áo mặc nhà
# ══════════════════════════════════════════════════════════════════
("nu", "Áo mặc nhà", "mac_nha", "bottom","Quần mặc nhà", "Áo mặc nhà + Quần mặc nhà: bộ nhà nữ"),
("nu", "Áo mặc nhà", "mac_nha", "bottom","Quần đùi cotton","Áo mặc nhà + Quần đùi: hè thoải mái"),
("be_gai","Áo mặc nhà", "mac_nha", "bottom","Quần mặc nhà", "Áo mặc nhà + Quần mặc nhà bé gái"),
("be_gai","Áo mặc nhà", "mac_nha", "bottom","Quần nỉ", "Áo mặc nhà + Quần nỉ: bé gái ấm"),
# ══════════════════════════════════════════════════════════════════
# 🏠 SET AS ANCHOR — Bộ mặc nhà (home set — outerwear only)
# ══════════════════════════════════════════════════════════════════
("nu", "Bộ mặc nhà", "mac_nha", "outerwear","Cardigan", "Bộ mặc nhà + Cardigan: ấm nhà mùa lạnh nữ"),
("nam", "Bộ mặc nhà", "mac_nha", "outerwear","Áo nỉ có mũ","Bộ mặc nhà + Hoodie: Nam thêm ấm"),
("be_trai","Bộ mặc nhà", "mac_nha", "outerwear","Áo nỉ", "Bộ mặc nhà + Áo nỉ: bé trai ấm"),
("be_gai", "Bộ mặc nhà", "mac_nha", "outerwear","Cardigan", "Bộ mặc nhà + Cardigan: bé gái ấm"),
# ══════════════════════════════════════════════════════════════════
# 🎽 SET AS ANCHOR — Bộ thể thao (sport set)
# ══════════════════════════════════════════════════════════════════
("unisex","Bộ thể thao", "the_thao", "outerwear","Áo khoác gió","Bộ thể thao + Áo khoác gió: warm-up layer"),
("nu", "Bộ thể thao", "the_thao", "outerwear","Áo khoác gió","Bộ thể thao nữ + Khoác gió"),
("nam", "Bộ thể thao", "the_thao", "outerwear","Áo khoác gió","Bộ thể thao nam + Khoác gió"),
# ══════════════════════════════════════════════════════════════════
# 👶 SET AS ANCHOR — Bộ quần áo (kid sets)
# ══════════════════════════════════════════════════════════════════
("be_trai","Bộ quần áo", "hang_ngay","outerwear","Áo khoác gió","Bộ quần áo bé trai + Khoác nhẹ"),
("be_gai", "Bộ quần áo", "di_choi", "outerwear","Áo khoác gió","Bộ quần áo bé gái + Khoác đi chơi"),
("nu", "Bộ quần áo", "hang_ngay","outerwear","Cardigan", "Bộ quần áo nữ + Cardigan: layered"),
("nam", "Bộ quần áo", "hang_ngay","outerwear","Áo nỉ có mũ","Bộ quần áo nam + Hoodie: casual set"),
# ══════════════════════════════════════════════════════════════════
# 👕 OUTERWEAR specific — Áo Polo for be_gai
# ══════════════════════════════════════════════════════════════════
("be_gai","Áo Polo", "hang_ngay","bottom","Quần leggings","Áo Polo bé gái + Leggings: sporty cute"),
("be_gai","Áo Polo", "hang_ngay","bottom","Quần soóc", "Áo Polo bé gái + Quần soóc: năng động"),
("be_gai","Áo Polo", "di_choi", "bottom","Chân váy", "Áo Polo bé gái + Chân váy: cute mix"),
# ══════════════════════════════════════════════════════════════════
# Quần body (underwear body suit — for office use)
# ══════════════════════════════════════════════════════════════════
("nu", "Quần body", "di_lam", "top", "Áo Sơ mi", "Quần body + Sơ mi: tucked-in office"),
("nu", "Quần body", "di_lam", "top", "Blazer", "Quần body + Blazer: power base layer"),
("nam", "Quần body", "mac_nha", "top", "Áo phông", "Quần body nam + Áo phông: thermal base"),
# ══════════════════════════════════════════════════════════════════
# Blazer (already seeded for di_lam, add di_choi + du_lich)
# ══════════════════════════════════════════════════════════════════
("nu", "Blazer", "di_choi", "bottom","Quần jean", "Blazer + Quần jean: effortless chic"),
("nu", "Blazer", "di_choi", "top", "Áo phông", "Blazer + Áo phông underneath: smart casual"),
("nu", "Blazer", "du_lich", "bottom","Quần âu", "Blazer du lịch: travel business chic"),
("nam", "Blazer", "di_choi", "bottom","Quần jean", "Blazer + Quần jean nam: casual smart"),
("nam", "Blazer", "di_choi", "top", "Áo phông", "Blazer + Áo phông nam: dressed down"),
# ══════════════════════════════════════════════════════════════════
# Cardigan (already seeded, add more coverage)
# ══════════════════════════════════════════════════════════════════
("nu", "Cardigan", "di_choi", "bottom","Quần jean", "Cardigan + Quần jean nữ: soft casual"),
("nu", "Cardigan", "mac_nha", "bottom","Quần mặc nhà","Cardigan + Quần mặc nhà: cozy home"),
("be_gai","Cardigan", "hang_ngay","bottom","Quần leggings","Cardigan bé gái + Leggings: soft look"),
# ══════════════════════════════════════════════════════════════════
# Áo giữ nhiệt (thermal)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo giữ nhiệt", "mac_nha", "bottom","Quần leggings","Áo giữ nhiệt + Leggings: warm layer nữ"),
("nam", "Áo giữ nhiệt", "mac_nha", "bottom","Quần nỉ", "Áo giữ nhiệt + Quần nỉ: thermal home"),
("all", "Áo giữ nhiệt", "hang_ngay","outerwear","Áo nỉ có mũ","Áo giữ nhiệt base + Hoodie phủ"),
# ══════════════════════════════════════════════════════════════════
# Áo ba lỗ (tank top)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo ba lỗ", "the_thao", "bottom","Quần leggings","Áo ba lỗ + Leggings: gym look nữ"),
("nu", "Áo ba lỗ", "the_thao", "bottom","Quần thể thao","Áo ba lỗ + Quần thể thao nữ"),
("nam", "Áo ba lỗ", "the_thao", "bottom","Quần thể thao","Áo ba lỗ + Quần thể thao: gym nam"),
("be_trai","Áo ba lỗ","the_thao", "bottom","Quần soóc", "Áo ba lỗ + Quần soóc: bé trai vận động"),
# ══════════════════════════════════════════════════════════════════
# Áo hai dây (nữ / bé gái — thêm các dịp còn thiếu)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo hai dây", "the_thao", "bottom","Quần leggings","Áo 2 dây + Leggings: activewear nữ"),
("nu", "Áo hai dây", "hang_ngay","bottom","Quần jean", "Áo 2 dây + Quần jean: chill hàng ngày"),
("be_gai","Áo hai dây", "du_lich", "bottom","Quần soóc", "Áo 2 dây bé gái + Quần soóc: biển"),
# ══════════════════════════════════════════════════════════════════
# Áo khoác lông vũ (down jacket)
# ══════════════════════════════════════════════════════════════════
("nam", "Áo khoác lông vũ","hang_ngay","top", "Áo phông", "Lông vũ + Áo phông: winter thermal"),
("nam", "Áo khoác lông vũ","hang_ngay","bottom","Quần jean", "Lông vũ + Quần jean: cold day comfort"),
("nu", "Áo khoác lông vũ","hang_ngay","bottom","Quần jean", "Lông vũ nữ + Quần jean: winter casual"),
("be_trai","Áo khoác lông vũ","hang_ngay","top", "Áo phông", "Lông vũ bé trai + Áo phông bên trong"),
("be_gai","Áo khoác lông vũ","di_choi", "bottom","Chân váy", "Lông vũ bé gái + Chân váy đi chơi"),
# ══════════════════════════════════════════════════════════════════
# Áo khoác chần bông (puffer jacket)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo khoác chần bông","hang_ngay","bottom","Quần jean","Chần bông + Quần jean: winter style nữ"),
("nu", "Áo khoác chần bông","di_choi", "bottom","Quần jean","Chần bông + Quần jean: đi chơi mùa đông"),
("be_gai","Áo khoác chần bông","hang_ngay","bottom","Quần nỉ", "Gilet chần bông + Quần nỉ bé gái"),
("be_trai","Áo khoác chần bông","hang_ngay","bottom","Quần nỉ", "Gilet chần bông + Quần nỉ bé trai"),
# ══════════════════════════════════════════════════════════════════
# Áo khoác nỉ không mũ (zip-up sweatshirt, no hood)
# ══════════════════════════════════════════════════════════════════
("nam", "Áo khoác nỉ không mũ","di_choi","bottom","Quần jean", "Khoác nỉ + Quần jean nam"),
("nam", "Áo khoác nỉ không mũ","hang_ngay","top", "Áo phông", "Khoác nỉ phủ Áo phông: smart casual"),
("nu", "Áo khoác nỉ không mũ","hang_ngay","bottom","Quần jean","Khoác nỉ nữ + Quần jean: casual"),
("unisex","Áo khoác nỉ không mũ","di_choi","bottom","Quần jean", "Khoác nỉ Unisex + Quần jean"),
# ══════════════════════════════════════════════════════════════════
# Áo khoác gió (windbreaker — already seeded as outerwear, now as anchor)
# ══════════════════════════════════════════════════════════════════
("nu", "Áo khoác gió","di_choi","bottom","Quần jean", "Áo khoác gió + Quần jean nữ"),
("nu", "Áo khoác gió","di_choi","top", "Áo phông", "Áo khoác gió phủ Áo phông nữ"),
("nam", "Áo khoác gió","di_choi","bottom","Quần jean", "Áo khoác gió + Quần jean nam"),
("nam", "Áo khoác gió","du_lich","bottom","Quần khaki", "Áo khoác gió + Quần khaki: travel ready"),
("unisex","Áo khoác gió","du_lich","top", "Áo phông", "Áo khoác gió + Áo phông: travel layer"),
("be_trai","Áo khoác gió","du_lich","bottom","Quần dài", "Áo khoác gió bé trai du lịch"),
("be_gai","Áo khoác gió","du_lich","bottom","Chân váy", "Áo khoác gió bé gái + Chân váy"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
inserted = 0
skipped = 0
for gender, anchor, occ, role, target, reason in RULES:
cur.execute(f"""
INSERT INTO {TABLE} (gender_target, anchor_category, occasion_tag, match_role, target_category, ai_reason)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT DO NOTHING
""", (gender, anchor, occ, role, target, reason))
if cur.rowcount > 0:
inserted += 1
else:
skipped += 1
conn.commit()
cur.close()
logger.info("[migrate003] Done: inserted=%d, skipped=%d, total=%d", inserted, skipped, len(RULES))
print(f"[OK] migrate_003 done: +{inserted} rules seeded ({skipped} already existed, {len(RULES)} total)")
except Exception as e:
if conn:
conn.rollback()
logger.error("[migrate003] Error: %s", e)
print(f"[ERROR] {e}")
finally:
if conn:
conn.close()
if __name__ == "__main__":
run()
"""
migrate_004_remaining.py — seed 27 anchor categories còn thiếu
"""
import logging, os, sys
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
TABLE = "dashboard_canifa.chatbot_fashion_rules"
RULES = [
# Áo khoác chần bông NAM
("nam", "Áo khoác chần bông", "hang_ngay","bottom","Quần jean", "Áo khoác chần bông + Quần jean nam"),
("nam", "Áo khoác chần bông", "di_choi", "bottom","Quần jean", "Áo khoác chần bông + Quần jean nam đi chơi"),
("nam", "Áo khoác chần bông", "hang_ngay","top", "Áo phông", "Áo khoác chần bông phủ Áo phông nam"),
# Áo Polo BE TRAI
("be_trai","Áo Polo", "hang_ngay","bottom","Quần soóc", "Áo Polo bé trai + Quần soóc hàng ngày"),
("be_trai","Áo Polo", "hang_ngay","bottom","Quần khaki", "Áo Polo bé trai + Quần khaki"),
("be_trai","Áo Polo", "du_lich", "bottom","Quần soóc", "Áo Polo bé trai + Quần soóc du lịch"),
# Áo Polo UNISEX
("unisex", "Áo Polo", "hang_ngay","bottom","Quần jean", "Áo Polo unisex + Quần jean"),
("unisex", "Áo Polo", "di_lam", "bottom","Quần khaki", "Áo Polo unisex + Quần khaki công sở"),
# Áo len BE TRAI
("be_trai","Áo len", "hang_ngay","bottom","Quần nỉ", "Áo len bé trai + Quần nỉ ấm"),
("be_trai","Áo len", "hang_ngay","bottom","Quần dài", "Áo len bé trai + Quần dài mùa lạnh"),
# Quần Khaki BE GAI
("be_gai", "Quần Khaki","hang_ngay","top", "Áo phông", "Quần Khaki bé gái + Áo phông"),
("be_gai", "Quần Khaki","di_choi", "top", "Áo kiểu", "Quần Khaki bé gái + Áo kiểu đi chơi"),
# Khăn / Scarf (accessories — recommend top/bottom to go with)
("unisex", "Khăn", "hang_ngay","top", "Áo phông", "Khăn + Áo phông: phụ kiện base"),
("unisex", "Khăn", "di_choi", "top", "Áo nỉ", "Khăn + Áo nỉ: mùa lạnh stylish"),
# Quần lót (underwear — minimal rules)
("nu", "Quần lót", "mac_nha", "top", "Áo phông", "Quần lót + Áo phông: lounge basic"),
("be_gai", "Quần lót", "mac_nha", "top", "Áo phông", "Quần lót bé gái + Áo phông nhà"),
# Áo khoác sợi NAM
("nam", "Áo khoác sợi","di_choi", "bottom","Quần jean", "Áo khoác sợi nam + Quần jean"),
("nam", "Áo khoác sợi","hang_ngay","top", "Áo phông", "Áo khoác sợi phủ Áo phông nam"),
# Pyjama BE TRAI
("be_trai","Pyjama", "mac_nha", "outerwear","Áo nỉ", "Pyjama bé trai + Áo nỉ mùa lạnh"),
# Áo khoác gilet chần bông (gilet)
("be_gai", "Áo khoác gilet chần bông","hang_ngay","top","Áo phông","Gilet chần bông + Áo phông bé gái"),
("be_gai", "Áo khoác gilet chần bông","hang_ngay","bottom","Quần nỉ","Gilet chần bông + Quần nỉ bé gái"),
("unisex", "Áo khoác gilet chần bông","hang_ngay","top","Áo phông", "Gilet chần bông + Áo phông unisex"),
("nu", "Áo khoác gilet chần bông","di_choi", "bottom","Quần jean","Gilet chần bông + Quần jean nữ"),
# Áo giữ nhiệt BE GAI
("be_gai", "Áo giữ nhiệt","mac_nha","bottom","Quần nỉ", "Áo giữ nhiệt + Quần nỉ bé gái ấm"),
("be_gai", "Áo giữ nhiệt","hang_ngay","outerwear","Áo nỉ", "Áo giữ nhiệt base + Áo nỉ phủ ngoài"),
# Quần giữ nhiệt NAM
("nam", "Quần giữ nhiệt","mac_nha","top","Áo giữ nhiệt","Quần giữ nhiệt + Áo giữ nhiệt base layer"),
("nam", "Quần giữ nhiệt","mac_nha","top","Áo phông", "Quần giữ nhiệt + Áo phông: thermal inside"),
# Áo nỉ UNISEX
("unisex", "Áo nỉ", "di_choi", "bottom","Quần jean", "Áo nỉ + Quần jean unisex casual"),
("unisex", "Áo nỉ", "hang_ngay","bottom","Quần dài", "Áo nỉ + Quần dài unisex chill"),
# Áo mặc nhà NAM
("nam", "Áo mặc nhà","mac_nha", "bottom","Quần mặc nhà","Áo mặc nhà + Quần mặc nhà: set nhà nam"),
("nam", "Áo mặc nhà","mac_nha", "bottom","Quần nỉ", "Áo mặc nhà + Quần nỉ nam winter home"),
# Quần váy BE GAI
("be_gai", "Quần váy", "di_choi", "top", "Áo phông", "Quần váy bé gái + Áo phông đi chơi"),
("be_gai", "Quần váy", "hang_ngay","top", "Áo kiểu", "Quần váy bé gái + Áo kiểu hàng ngày"),
# Khăn mặt (towel) — non-fashion, skip meaningful pairing; add minimal
("all", "Khăn mặt", "mac_nha", "top", "Áo mặc nhà", "Khăn mặt + Áo mặc nhà: home essentials"),
# Bộ quần áo UNISEX
("unisex", "Bộ quần áo","hang_ngay","outerwear","Áo khoác gió","Bộ quần áo unisex + Khoác nhẹ"),
# Áo khoác gilet chần bông WOMEN (đã có? thêm thêm)
("nu", "Áo khoác gilet chần bông","di_choi","top","Áo phông","Gilet chần bông nữ + Áo phông base"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
inserted = 0
for gender, anchor, occ, role, target, reason in RULES:
cur.execute(f"""
INSERT INTO {TABLE} (gender_target, anchor_category, occasion_tag, match_role, target_category, ai_reason)
VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING
""", (gender, anchor, occ, role, target, reason))
if cur.rowcount > 0:
inserted += 1
conn.commit()
cur.close()
print(f"[OK] migrate_004 done: +{inserted} rules seeded ({len(RULES)} total in batch)")
except Exception as e:
if conn: conn.rollback()
print(f"[ERROR] {e}")
finally:
if conn: conn.close()
if __name__ == "__main__":
run()
"""migrate_005_final.py — seed 11 anchor categories còn lại"""
import logging, os, sys
logging.basicConfig(level=logging.INFO)
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
TABLE = "dashboard_canifa.chatbot_fashion_rules"
RULES = [
# Tất (socks) — minimal, recommend outfit underneath
("nam", "Tất", "the_thao","top", "Áo phông", "Tất + Áo phông: sport set hoàn chỉnh"),
("nam", "Tất", "di_lam", "top", "Áo Sơ mi", "Tất + Sơ mi: office accessory"),
("nu", "Tất", "the_thao","top", "Áo phông", "Tất thể thao + Áo phông nữ"),
("nu", "Tất", "di_lam", "top", "Blouse", "Tất + Blouse office nữ"),
("unisex","Tất", "the_thao","top", "Áo phông", "Tất + Áo phông: universal sport"),
("be_trai","Tất","the_thao","top", "Áo phông", "Tất bé trai + Áo phông thể thao"),
# Áo khoác (generic)
("nu", "Áo khoác","di_choi","bottom","Quần jean","Áo khoác nữ + Quần jean"),
("nu", "Áo khoác","du_lich","bottom","Quần jean","Áo khoác nữ + Quần jean du lịch"),
# Áo khoác gilet chần bông BE TRAI
("be_trai","Áo khoác gilet chần bông","hang_ngay","top","Áo phông","Gilet bé trai + Áo phông bên trong"),
("be_trai","Áo khoác gilet chần bông","hang_ngay","bottom","Quần nỉ","Gilet bé trai + Quần nỉ đông"),
# Khẩu trang (mask — non-fashion, add minimal)
("unisex","Khẩu trang","hang_ngay","top","Áo phông", "Khẩu trang + Áo phông: daily protection"),
# Quần lót đùi NAM
("nam", "Quần lót đùi","mac_nha","top","Áo phông", "Quần lót đùi + Áo phông: home basic nam"),
# Túi xách (bag — accessory, no forced bottom/top)
("unisex","Túi xách","di_choi","top", "Áo phông", "Túi xách + Áo phông: streetwear complete"),
("nu", "Túi xách","di_choi","top", "Áo kiểu", "Túi xách + Áo kiểu nữ: styled"),
# Áo nỉ có mũ BE GAI
("be_gai","Áo nỉ có mũ","hang_ngay","bottom","Quần leggings","Hoodie bé gái + Leggings"),
("be_gai","Áo nỉ có mũ","hang_ngay","bottom","Quần nỉ", "Hoodie bé gái + Quần nỉ ấm"),
("be_gai","Áo nỉ có mũ","di_choi", "bottom","Quần jean", "Hoodie bé gái + Quần jean đi chơi"),
# Áo mặc nhà BE TRAI
("be_trai","Áo mặc nhà","mac_nha","bottom","Quần mặc nhà","Áo mặc nhà + Quần mặc nhà bé trai"),
("be_trai","Áo mặc nhà","mac_nha","bottom","Quần nỉ", "Áo mặc nhà bé trai + Quần nỉ"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
inserted = 0
for gender, anchor, occ, role, target, reason in RULES:
cur.execute(f"""
INSERT INTO {TABLE}(gender_target,anchor_category,occasion_tag,match_role,target_category,ai_reason)
VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING
""", (gender, anchor, occ, role, target, reason))
if cur.rowcount > 0:
inserted += 1
conn.commit(); cur.close()
print(f"[OK] migrate_005 done: +{inserted}/{len(RULES)} rules seeded")
except Exception as e:
if conn: conn.rollback()
print(f"[ERROR] {e}")
finally:
if conn: conn.close()
if __name__ == "__main__":
run()
"""migrate_006_edge_cases.py — seed 11 edge anchor categories còn lại"""
import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
TABLE = "dashboard_canifa.chatbot_fashion_rules"
RULES = [
# Áo Sơ mi BE TRAI
("be_trai","Áo Sơ mi", "di_lam", "bottom","Quần khaki", "Sơ mi bé trai + Quần khaki đi học"),
("be_trai","Áo Sơ mi", "hang_ngay","bottom","Quần jean", "Sơ mi bé trai + Quần jean hàng ngày"),
# Áo len gilet BE GAI
("be_gai", "Áo len gilet","hang_ngay","top", "Áo phông", "Gilet len bé gái + Áo phông bên trong"),
("be_gai", "Áo len gilet","hang_ngay","bottom","Quần leggings","Gilet len bé gái + Leggings ấm áp"),
# Tất BE GAI
("be_gai","Tất", "the_thao", "top", "Áo phông", "Tất bé gái + Áo phông thể thao"),
# Áo khoác nỉ có mũ NAM (với schema tên khác)
("nam", "Áo khoác nỉ có mũ","di_choi","bottom","Quần jean","Khoác nỉ có mũ nam + Quần jean đi chơi"),
("nam", "Áo khoác nỉ có mũ","hang_ngay","top","Áo phông", "Khoác nỉ có mũ nam + Áo phông bên trong"),
# Áo khoác nỉ có mũ BE TRAI
("be_trai","Áo khoác nỉ có mũ","hang_ngay","bottom","Quần nỉ","Khoác nỉ có mũ bé trai + Quần nỉ"),
("be_trai","Áo khoác nỉ có mũ","di_choi", "bottom","Quần jean","Khoác nỉ có mũ bé trai + Quần jean"),
# Khăn mặt (others→unisex)
("all", "Khăn mặt", "mac_nha", "top", "Áo mặc nhà", "Khăn mặt + Áo mặc nhà: home essential"),
# Pyjama BE GAI
("be_gai","Pyjama", "mac_nha", "outerwear","Cardigan", "Pyjama bé gái + Cardigan ấm nhà"),
# Pyjama NU
("nu", "Pyjama", "mac_nha", "outerwear","Cardigan", "Pyjama nữ + Cardigan mùa lạnh"),
# Quần váy NU
("nu", "Quần váy", "di_choi", "top", "Áo phông", "Quần váy nữ + Áo phông đi chơi"),
("nu", "Quần váy", "di_lam", "top", "Blouse", "Quần váy nữ + Blouse văn phòng"),
# Găng tay chống nắng (sun glove — aesthetic tiny)
("unisex","Găng tay chống nắng","du_lich","top","Áo phông", "Găng tay chống nắng + Áo phông du lịch"),
("nu", "Găng tay chống nắng","du_lich","top","Áo phông", "Găng tay chống nắng + Áo phông nữ"),
# Áo khoác gilet chần bông NAM
("nam", "Áo khoác gilet chần bông","hang_ngay","top","Áo phông", "Gilet chần bông nam + Áo phông"),
("nam", "Áo khoác gilet chần bông","hang_ngay","bottom","Quần jean","Gilet nam + Quần jean"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
inserted = 0
for r in RULES:
cur.execute(f"""
INSERT INTO {TABLE}(gender_target,anchor_category,occasion_tag,match_role,target_category,ai_reason)
VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING
""", r)
if cur.rowcount > 0:
inserted += 1
conn.commit(); cur.close()
print(f"[OK] migrate_006 done: +{inserted}/{len(RULES)} rules seeded")
except Exception as e:
if conn: conn.rollback()
print(f"[ERROR] {e}")
finally:
if conn: conn.close()
if __name__ == "__main__":
run()
"""migrate_007_absolute_final.py — 6 anchor categories cuối cùng"""
import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
TABLE = "dashboard_canifa.chatbot_fashion_rules"
RULES = [
("be_gai","Áo lót", "mac_nha", "bottom","Quần mặc nhà", "Áo lót bé gái + Quần mặc nhà nhà"),
("be_gai","Áo lót", "mac_nha", "top", "Áo mặc nhà", "Áo lót bé gái base + Áo mặc nhà phủ"),
("nam", "Quần lót tam giác", "mac_nha", "top", "Áo phông", "Quần lót + Áo phông: home basic nam"),
("be_gai","Áo Body", "di_lam", "bottom","Chân váy", "Áo Body bé gái + Chân váy: cute look"),
("be_gai","Áo Body", "hang_ngay","bottom","Quần leggings", "Áo Body bé gái + Leggings hàng ngày"),
("be_gai","Quần leggings mặc nhà","mac_nha", "top", "Áo phông", "Leggings nhà bé gái + Áo phông"),
("be_gai","Quần leggings mặc nhà","mac_nha", "top", "Áo mặc nhà", "Leggings nhà bé gái + Áo mặc nhà"),
("be_trai","Blazer", "di_lam", "bottom","Quần khaki", "Blazer bé trai + Quần khaki đi học"),
("be_trai","Blazer", "di_lam", "top", "Áo Sơ mi", "Blazer bé trai mặc ngoài Sơ mi"),
("nu", "Áo khoác sợi", "di_choi", "bottom","Quần jean", "Áo khoác sợi nữ + Quần jean"),
("nu", "Áo khoác sợi", "hang_ngay","top", "Áo phông", "Áo khoác sợi nữ + Áo phông base"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
inserted = 0
for r in RULES:
cur.execute(f"""
INSERT INTO {TABLE}(gender_target,anchor_category,occasion_tag,match_role,target_category,ai_reason)
VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING
""", r)
if cur.rowcount > 0:
inserted += 1
conn.commit(); cur.close()
print(f"[OK] migrate_007 done: +{inserted}/{len(RULES)} rules seeded")
except Exception as e:
if conn: conn.rollback()
print(f"[ERROR] {e}")
finally:
if conn: conn.close()
if __name__ == "__main__":
run()
"""migrate_008_done.py — 5 anchor categories cuối cùng (Áo Body, Bộ thể thao bé trai, ...)"""
import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
TABLE = "dashboard_canifa.chatbot_fashion_rules"
RULES = [
("nu", "Áo Body", "di_lam", "bottom","Quần âu", "Áo Body nữ + Quần âu: bodysuit office"),
("nu", "Áo Body", "di_lam", "bottom","Chân váy", "Áo Body nữ + Chân váy: sleek look"),
("nu", "Áo Body", "di_choi", "bottom","Quần jean", "Áo Body nữ + Quần jean: casual chic"),
("unisex","Áo Body", "the_thao", "bottom","Quần leggings","Áo Body + Leggings: activewear"),
("nam", "Áo Body", "the_thao", "bottom","Quần thể thao","Áo Body nam + Quần thể thao gym"),
("be_trai","Bộ thể thao","the_thao","outerwear","Áo khoác gió","Bộ thể thao bé trai + Khoác gió"),
("be_trai","Bộ thể thao","the_thao","outerwear","Áo nỉ có mũ","Bộ thể thao bé trai + Hoodie"),
("unisex","Quần mặc nhà","mac_nha","top","Áo phông", "Quần mặc nhà unisex + Áo phông nhà"),
("unisex","Quần mặc nhà","mac_nha","top","Áo mặc nhà", "Quần mặc nhà unisex + Áo mặc nhà"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
inserted = 0
for r in RULES:
cur.execute(f"""
INSERT INTO {TABLE}(gender_target,anchor_category,occasion_tag,match_role,target_category,ai_reason)
VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING
""", r)
if cur.rowcount > 0:
inserted += 1
conn.commit(); cur.close()
print(f"[OK] migrate_008 done: +{inserted}/{len(RULES)} rules seeded")
except Exception as e:
if conn: conn.rollback()
print(f"[ERROR] {e}")
finally:
if conn: conn.close()
if __name__ == "__main__":
run()
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common.pool_wrapper import get_pooled_connection_compat
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute("SELECT anchor_category FROM chatbot_fashion_rules WHERE anchor_category LIKE '%Chân váy%'")
print("LIKE:", cur.fetchall())
cur.execute("SELECT anchor_category FROM chatbot_fashion_rules WHERE UPPER(anchor_category) = UPPER('Chân váy')")
print("UPPER:", cur.fetchall())
cur.execute("SELECT anchor_category FROM chatbot_fashion_rules WHERE LOWER(anchor_category) = LOWER('Chân váy')")
print("LOWER:", cur.fetchall())
"""
tests/test_fashion_rules_batch.py
Batch-test 100 SP ngẫu nhiên → kiểm tra fashion rule coverage & integrity.
Run:
cd backend
$env:PYTHONIOENCODING="utf-8"
.venv\Scripts\python.exe tests/test_fashion_rules_batch.py --limit 100
"""
import sys
import os
import random
import argparse
from collections import defaultdict
# ── path ──────────────────────────────────────────────────────────────────────
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from worker.stylist_engine import StylistEngine
from common.pool_wrapper import get_pooled_connection_compat
def fetch_all_anchor_categories():
"""Lấy tập hợp (anchor_category, gender_target) đã có rules trong DB."""
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute("""\
SELECT DISTINCT anchor_category, gender_target
FROM dashboard_canifa.chatbot_fashion_rules
WHERE gender_target != 'all'
""")
rows = cur.fetchall()
cur.close()
return {(r[0].strip().lower(), r[1].strip().lower()) for r in rows}
except Exception as e:
print(f"[DB ERROR] {e}")
return set()
finally:
if conn:
conn.close()
def run_batch_test(limit: int = 100, verbose: bool = False):
engine = StylistEngine()
catalog = engine._load_catalog()
if not catalog:
print("Catalog rong!")
return
print(f"Tong so SP trong catalog: {len(catalog)}")
# Chỉ lấy SP có product_line + gender
valid = [p for p in catalog if p.get("product_line") and p.get("gender")]
samples = random.sample(valid, min(limit, len(valid)))
print(f"Test {len(samples)} SP ngau nhien...\n")
# Anchor categories có rules trong DB (lowercase)
db_anchors = fetch_all_anchor_categories()
stats = {
"total": len(samples),
"passed": 0,
"no_rules_in_db": 0,
"empty_all_occasions": 0,
"rule_violations": 0,
}
# Thống kê anchor nào không có rules
missing_anchors: dict[str, list[str]] = defaultdict(list)
violation_details: list[str] = []
empty_details: list[str] = []
for p in samples:
code = p.get("code", "?")
anchor_cat = p.get("product_line", "")
gender_raw = p.get("gender", "")
gender_norm = engine._normalize_gender(gender_raw)
# Kiểm tra xem DB có rules cho combo này không
anchor_lower = anchor_cat.strip().lower()
has_specific = (anchor_lower, gender_norm) in db_anchors
has_all = (anchor_lower, "all") in db_anchors
if not has_specific and not has_all:
stats["no_rules_in_db"] += 1
missing_anchors[f"{anchor_cat} [{gender_raw}→{gender_norm}]"].append(code)
continue
# Gọi engine
ai_matches = engine.compute_dynamic_rule_matches(code)
db_rules = engine._fetch_rules_with_reason(anchor_cat, gender_raw)
# allowed = {occ: set(target_cat_lower)}
allowed_by_occ: dict[str, set] = defaultdict(set)
for r in db_rules:
allowed_by_occ[r["occ"]].add(r["target_cat"].strip().lower())
is_all_empty = True
violations = []
for occ, allowed_targets in allowed_by_occ.items():
occ_data = ai_matches.get(occ, {})
if not occ_data:
continue
is_all_empty = False
for role, items in occ_data.items():
for item in items:
item_cat = item.get("product_line", "").strip().lower()
if item_cat not in allowed_targets:
violations.append(
f" Dip [{occ}] role={role}: recommend '{item_cat}' | Cho phep: {allowed_targets}"
)
if is_all_empty:
stats["empty_all_occasions"] += 1
empty_details.append(f" {anchor_cat} [{gender_norm}] ({code}) -> Toan bo dip TRONG (mau khong khop)")
if violations:
stats["rule_violations"] += 1
violation_details.append(f" {anchor_cat} [{gender_norm}] ({code}):")
violation_details.extend(violations)
else:
if not is_all_empty:
stats["passed"] += 1
# ── REPORT ──────────────────────────────────────────────────────────────
sep = "=" * 55
print(sep)
print(" KET QUA KIEM THU FASHION RULES")
print(sep)
print(f"Tong test : {stats['total']}")
print(f"Passed (ok) : {stats['passed']}")
print(f"Khong co rules DB : {stats['no_rules_in_db']} <- can seed them")
print(f"Rules co, nhung trong: {stats['empty_all_occasions']} <- catalog mau chua du")
print(f"Vi pham rules : {stats['rule_violations']} <- loi engine")
print()
# Thống kê anchor thiếu rules
if missing_anchors:
print(f"--- ANCHOR CATEGORY CHUA SEED RULES ({len(missing_anchors)} loai) ---")
sorted_missing = sorted(missing_anchors.items(), key=lambda x: -len(x[1]))
for anchor, codes in sorted_missing[:20]:
print(f" {len(codes):3d} SP | {anchor}")
if len(sorted_missing) > 20:
print(f" ... va {len(sorted_missing) - 20} loai khac")
print()
if violation_details:
print(f"--- VI PHAM RULES ({stats['rule_violations']} SP) ---")
for line in violation_details[:30]:
print(line)
print()
if verbose and empty_details:
print("--- RONG DO MAU (verbose) ---")
for line in empty_details[:20]:
print(line)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--limit", type=int, default=100)
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
run_batch_test(args.limit, args.verbose)
import sqlite3
import os
db_path = "backend/db/memos.db"
if not os.path.exists(db_path):
print(f"❌ Database not found at {db_path}")
# Try alternate path
db_path = "backend/database/cuccu_note.db"
if not os.path.exists(db_path):
print(f"❌ Database not found at {db_path} either")
exit(1)
print(f"✅ Found database at {db_path}")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
print("Tables in database:")
for table in tables:
print(f"- {table[0]}")
conn.close()
import subprocess
import json
import time
BASE_URL = "http://localhost:5100"
def run_curl(method, endpoint, data=None, token=None):
cmd = ["curl", "-s", "-X", method, f"{BASE_URL}{endpoint}"]
cmd += ["-H", "Content-Type: application/json"]
if token:
cmd += ["-H", f"Authorization: Bearer {token}"]
if data:
cmd += ["-d", json.dumps(data)]
result = subprocess.run(cmd, capture_output=True, text=True)
try:
return json.loads(result.stdout)
except:
return result.stdout
def test_api():
print("Waiting for server to be ready...")
# Simple wait-loop
for _ in range(30):
try:
res = run_curl("GET", "/api/v1/memos")
if isinstance(res, list) or "detail" in res:
print("✅ Server is UP")
break
except:
pass
time.sleep(2)
else:
print("❌ Server timed out")
return
# 1. Register
username = f"testuser_{int(time.time())}"
print(f"Testing Register with {username}...")
reg_data = {
"username": username,
"email": f"{username}@example.com",
"password": "testpassword123"
}
reg_res = run_curl("POST", "/api/v1/auth/register", data=reg_data)
print("Register Response:", json.dumps(reg_res, indent=2))
if "access_token" not in reg_res:
print("❌ Registration failed")
return
token = reg_res["access_token"]
# 2. Create Memo
print("Testing Create Memo...")
memo_data = {
"content": "Hello world from API test!",
"visibility": "PRIVATE"
}
memo_res = run_curl("POST", "/api/v1/memos", data=memo_data, token=token)
print("Create Memo Response:", json.dumps(memo_res, indent=2))
# 3. List Memos
print("Testing List Memos...")
list_res = run_curl("GET", "/api/v1/memos", token=token)
print(f"List Memos found {len(list_res)} memos")
if __name__ == "__main__":
test_api()
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