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

feat(ai-stylist): refactor engine to DB rules, add SQLite mock, seed 51...

feat(ai-stylist): refactor engine to DB rules, add SQLite mock, seed 51 fashion rules, add tests + plan docs
parent 2ce88d1a
---
name: "TDD Auto-Sandbox Loop"
description: "Vòng lặp tự động hóa việc phát triển tính năng bằng cách viết Script thử nghiệm (Test-Driven Sandbox), cho AI tự chạy, tự đọc lỗi, tự sửa code đến khi đầu ra chuẩn 100% trước khi ghép vào ứng dụng chính."
---
# TDD Auto-Sandbox Loop (Vòng lặp Tự Động Thử Nghiệm)
Sử dụng workflow này khi User yêu cầu: **"Xây dựng một tính năng mới", "Kiểm tra và sửa lỗi logic", "Tạo luồng kết nối DB mới (chọc SQL)"**, hoặc User trực tiếp gọi lệnh `/auto-tdd`.
## MỤC TIÊU
- TUYỆT ĐỐI KHÔNG sửa file API lõi (`router.py`, `engine.py`,...) ngay lập tức để tránh làm "chết" server hoặc sập Production.
- Mô phỏng và chạy thử nghiệm mọi logic mới hoàn toàn độc lập trong thư mục Sandbox (`backend/scripts/`).
- Tận dụng khả năng "chữa lành" (self-healing) của AI: Tự chạy script -> Tự đọc lỗi -> Tự fix code -> Lặp lại đến khi kết quả Data (Output) chính xác tuyệt đối.
- Chỉ "Cấy" (Inject) code vào Source chính khi User đã xác nhận hoặc Output Sandbox đã đạt 100% tỉ lệ thành công.
---
## 🛠 CÁC BƯỚC THỰC HIỆN BẮT BUỘC
### Bước 1: Phân tích & Viết Script Rác (Sandbox Setup)
1. Xác định rõ Input (Đầu vào) và Expected Output (Kết quả mong muốn) của User.
2. Tạo 1 file Python độc lập trong thư mục `backend/scripts/` có tên phản ánh tính năng.
*(Ví dụ: `backend/scripts/test_fetch_stock_logic.py`, `backend/scripts/scratch_sql_query.py`)*
3. Setup file này sao cho nó **có thể tự chạy được (`__main__`)**, tự import các common function, DB connections cần thiết từ hệ thống, và trả log kết quả ra cửa sổ Console (sử dụng in mã UTF-8 bằng `sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')`).
### Bước 2: Kích hoạt Vòng Lặp Chữa Lành (Self-Healing Auto-Run)
- **// turbo-all**
1. AI sử dụng công cụ Tự chạy Command (Terminal) để thực thi file python rác vừa tạo.
2. **KẾT QUẢ > LỖI (Exception/Syntax Error):** AI đọc Terminal Error -> Hiểu nguyên nhân -> Tự sửa lại file Test -> Chạy lệnh lại.
3. **KẾT QUẢ > SAI LOGIC (Data rác, không map đúng Rule):** AI phân tích Output thực tế vs Output kì vọng -> Tìm hiểu Rule hệ thống -> Tự sửa lại file Test -> Chạy lệnh lại.
4. Quá trình này **không cần sự can thiệp của User**, AI liên tục chạy lệnh và fix đến khi nào màn hình kết quả Terminal (Data in ra) đúng 100% logic.
### Bước 3: Show kết quả cho User Duyệt
- Sau khi có cái "Mộc xanh" trên Terminal (Output chuẩn).
- AI chụp (hoặc copy paste) cái Output Terminal đó gửi cho User kèm lời báo cáo: *"Script đã chạy thành công, log data chuẩn. Bro check xem ưng ý chưa để tôi cấy vào hệ thống chính?"*
- Nếu User OK -> Lên Bước 4.
- Nếu User muốn điều chỉnh -> Lại vòng về Bước 2.
### Bước 4: Cấy Ghép Vào Hệ Thống (Production Injection)
- Mở file hệ thống lõi (ví dụ: `worker/stylist_engine.py`, `api/router.py`).
- Triển khai chức năng đã TEST THÀNH CÔNG từ file Sandbox vào hàm chuẩn.
- Lưu ý khi cấy phải giữ nguyên toàn bộ chuẩn biến số của hàm hiện tại.
- Xóa các lệnh in rác (`print`) ở Sandbox và thay bằng `logger.info()` chuẩn.
### Bước 5: Cleanup & Commit
- Cleanup code thừa, chuyển file rác trong `scripts/` về trạng thái đọc tắt comment nếu cần thiết (hoặc đổi tên thành `archive_...` nếu Task yêu cầu luỹ kế) để giữ gọn Project backend.
## ⚠️ QUY TẮC SỐNG CÒN CỦA WORKFLOW
- **CHẶN SỬA MÙ:** Cấm sửa trực tiếp hệ thống logic vòng lặp lớn (như thuật toán Fashion Stylist Engine) khi chưa chạy Test Script Sandbox để kiểm chứng 1 input nhỏ lẻ!
- **ENCODING LUÔN LUÔN UTF-8:** Database Canifa chứa rất nhiều Tiếng Việt có dấu, khi in `print()` ra Windows Powershell thường bị lỗi charmap. Phải cài cắm header cho file Sandbox:
```python
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
```
- **TỰ CHỦ ĐỘNG:** Đừng rụt rè gõ ra để hỏi User là lỗi này phải sửa sao. Dùng Tool `run_command` fix liên tục cho đến khi gõ chạy Terminal ngon êm ru nhé!
---
name: Local Database Proxy API Tester
description: Vòng lặp test toàn bộ các API endpoint có kết nối Database (Postgres/StarRocks) qua SQLite Mock Proxy để phát hiện lỗi Dialect và sửa Regex.
---
# Local DB Proxy API Test Workflow
Workflow này dùng để tự động dò quét, curl test và vá lỗi cho toàn bộ các API có giao tiếp với Database thông qua lớp mạo danh `sqlite_mock.py`.
## Bước 1: Liệt kê các API giao tiếp với Database
- Quét nhanh thư mục `backend/api` để tìm các route gọi `execute_query` (StarRocks) hoặc `db_pool.get_conn()` (Postgres).
- Lập danh sách các API quan trọng cần test, điển hình:
- `/api/product-desc/list`
- `/api/product-desc/overview`
- `/api/products/filters`
- `/api/products/list`
- Báo cáo/Fashion rules routes.
## Bước 2: Test từng Endpoint (với Uvicorn đang chạy)
Dùng `run_command` để gọi PowerShell `Invoke-RestMethod` (hoặc curl) vào từng endpoint (http://127.0.0.1:5000).
```markdown
// turbo
Invoke-RestMethod -Uri "http://127.0.0.1:5000/api/product-desc/list?limit=10" -Method Get
```
## Bước 3: Đọc Log và Sửa Lỗi (Diagnose & Patch)
Nếu endpoint trả về lỗi `500 Internal Server Error`, Agent bắt buộc phải:
1. Đọc nội dung log lỗi trên Terminal Uvicorn đang rớt.
2. Kiểm tra xem lỗi xuất phát từ SQLite không hiểu cú pháp (ví dụ: thiếu Regex `?` hoặc sai tên bảng).
3. Quay lại file `backend/common/sqlite_mock.py` để bổ sung thuật toán `re.sub()` hoặc logic thay thế trong `translate_query()`.
4. Sau khi sửa mô-đun, chờ Uvicorn nháy reload và vòng lại Bước 2 test lại chính Endpoint đó.
## Bước 4: Validation (Xanh lè toàn bộ)
Khi toàn bộ API endpoint liệt kê ở Bước 1 đều trả về HTTP 200 (có chèn Data) với SQLite cục bộ, đánh dấu quá trình test kết thúc thành công.
...@@ -55,3 +55,7 @@ Thumbs.db ...@@ -55,3 +55,7 @@ Thumbs.db
run.txt run.txt
backend/agent/tools/query.txt backend/agent/tools/query.txt
backend/schema_dump.json backend/schema_dump.json
# SQLite local mock DB (rebuilt from backend/database/postgres/ + starrocks/ SQL dumps)
*.sqlite
*.sqlite-journal
#!/usr/bin/env python
"""
Batch test: tạo catalog nhỏ từ DB và chạy _compute_matches cho 1 sản phẩm.
"""
import sys, os, json, sqlite3
# MUST set before importing engine
os.environ['USE_LOCAL_SQLITE'] = 'True'
sys.path.insert(0, os.path.dirname(__file__))
sys.stdout.reconfigure(encoding='utf-8') if hasattr(sys.stdout, 'reconfigure') else None
from worker.stylist_engine import StylistEngine
print("=== BATCH TEST (Simplified) ===\n")
engine = StylistEngine()
print(f"[+] Engine loaded. Rules: {len(engine.rules['occasions'])} occasions, weights: {engine.rules['score_weights']}")
catalog = engine._get_catalog()
print(f"[+] Catalog size: {len(catalog)} products")
if len(catalog) == 0:
print("[!] Catalog empty - check DB connections (StarRocks/Postgres)")
sys.exit(1)
# Get valid anchor categories from SQLite DB (seeded rules)
import sqlite3
conn_sqlite = sqlite3.connect('database/canifa_ai_dump.sqlite')
cur_sqlite = conn_sqlite.cursor()
cur_sqlite.execute("SELECT DISTINCT anchor_category FROM chatbot_fashion_rules")
valid_anchors = {row[0].lower() for row in cur_sqlite.fetchall()}
cur_sqlite.close()
conn_sqlite.close()
print(f"[+] Valid anchor categories from DB: {len(valid_anchors)} categories")
test_product = None
for p in catalog:
pl = p.get("product_line", "").lower()
if pl in valid_anchors:
test_product = p
break
if not test_product:
print("[!] No catalog product matches any anchored product line in DB rules")
print("[!] Available anchors:", valid_anchors)
sys.exit(1)
print(f"[+] Testing with product code: {test_product['code']}")
print(f" Product line: {test_product['product_line']}")
print(f" Color: {test_product['color']}")
print(f" Gender: {test_product['gender']}")
print("\n[+] Running _compute_matches...")
matches = engine._compute_matches(test_product, catalog[:50])
print(f"\n[+] Result: {len(matches)} occasions generated")
for occ, roles in matches.items():
print(f"\n Occasion: {occ}")
for role, items in roles.items():
print(f" {role}: {len(items)} items")
for item in items[:2]:
print(f" - {item['code']} ({item['product_line']}) score={item['score']}")
print("\n[+] Batch test completed successfully!")
import os
import sqlite3
import logging
from typing import Optional, Any
from contextlib import contextmanager
logger = logging.getLogger(__name__)
class SQLiteDBManager:
"""
Quản lý kết nối tới file SQLite database (canifa_ai_dump.sqlite).
Được thiết kế theo Singleton pattern để dùng thống nhất trong toàn bộ App FastAPI.
"""
_instance = None
_db_path = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(SQLiteDBManager, cls).__new__(cls)
# Default path trỏ thẳng vào: backend/database/canifa_ai_dump.sqlite
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
cls._instance._db_path = os.path.join(base_dir, "database", "canifa_ai_dump.sqlite")
return cls._instance
@property
def db_path(self) -> str:
return self._db_path
@db_path.setter
def db_path(self, path: str):
self._db_path = path
def get_connection(self) -> sqlite3.Connection:
"""
Khởi tạo và trả về 1 kết nối SQLite.
Sử dụng check_same_thread=False để đảm bảo an toàn khi chạy trên đa luồng của FastAPI.
"""
if not os.path.exists(self._db_path):
logger.warning(f"[SQLite] Cảnh báo: File database không tồn tại tại {self._db_path}")
conn = sqlite3.connect(self._db_path, check_same_thread=False)
# Row factory giúp lấy dữ liệu dưới dạng Dictionary (giống psycopg) thay vì Tuple index
conn.row_factory = sqlite3.Row
return conn
@contextmanager
def session(self):
"""
Context manager để tự động tự động đóng Connection sau khi dùng xong (tránh lock DB).
Sử dụng:
with sqlite_db.session() as conn:
conn.execute(...)
"""
conn = self.get_connection()
try:
yield conn
finally:
conn.close()
def fetch_all(self, query: str, params: tuple = ()) -> list[dict]:
"""Hàm tiện ích: Lọc toàn bộ danh sách, trả về list chứa các dict."""
with self.session() as conn:
cursor = conn.cursor()
cursor.execute(query, params)
rows = cursor.fetchall()
return [dict(row) for row in rows]
def fetch_one(self, query: str, params: tuple = ()) -> Optional[dict]:
"""Hàm tiện ích: Lấy duy nhất 1 item đầu tiên, trả về dict."""
with self.session() as conn:
cursor = conn.cursor()
cursor.execute(query, params)
row = cursor.fetchone()
return dict(row) if row else None
def execute(self, query: str, params: tuple = ()) -> int:
"""Hàm tiện ích: Chạy các truy vấn INSERT/UPDATE/DELETE và trả về số dòng bị ảnh hưởng."""
with self.session() as conn:
cursor = conn.cursor()
cursor.execute(query, params)
conn.commit()
return cursor.rowcount
# Export một Singleton instance để các file khác import vào dùng luôn
# Cách dùng: from common.sqlite_db import sqlite_db
sqlite_db = SQLiteDBManager()
import re
import logging
from contextlib import contextmanager
from .sqlite_db import sqlite_db
logger = logging.getLogger(__name__)
def translate_query(query: str) -> str:
"""
Dịch câu SQL đặc thù của Postgres/StarRocks sang dạng mà SQLite hiểu được.
"""
# 1. Thay thế Placeholder của Postgres/StarRocks (%s) thành của SQLite (?)
q = query.replace("%s", "?")
# 2. Thay thế Postgres Schema (dashboard_canifa)
q = re.sub(r'"dashboard_canifa"\."([a-zA-Z0-9_]+)"', r'pg__dashboard_canifa__\1', q)
q = re.sub(r'dashboard_canifa\.([a-zA-Z0-9_]+)', r'pg__dashboard_canifa__\1', q)
# Thay thế Postgres Schema (canifa_chat)
q = re.sub(r'"canifa_chat"\."([a-zA-Z0-9_]+)"', r'pg__canifa_chat__\1', q)
q = re.sub(r'canifa_chat\.([a-zA-Z0-9_]+)', r'pg__canifa_chat__\1', q)
# Thay thế Postgres Schema (public)
q = re.sub(r'"public"\."([a-zA-Z0-9_]+)"', r'pg__public__\1', q)
q = re.sub(r'public\.([a-zA-Z0-9_]+)', r'pg__public__\1', q)
# 3. Thay thế cấu trúc của StarRocks
# Mẫu query thường gặp: shared_source.magento_product_dimension_with_text_embedding
# Hoặc test_db.magento_product_...
# Hoặc magento_product_...
q = re.sub(r'([a-zA-Z0-9_]+\.)?`?magento_product_dimension_with_text_embedding`?', r'sr__test_db__magento_product_dimension_with_text_embedding', q)
# 4. Vá các ngữ pháp (Dialect) bị lệch pha
# ANY_VALUE(col) -> MAX(col)
q = re.sub(r'ANY_VALUE\s*\(', r'MAX(', q, flags=re.IGNORECASE)
# IF(cond, true, false) -> IIF(cond, true, false) của SQLite
# Lưu ý: Tìm chữ IF đứng đầu cẩn thận dính chuỗi
q = re.sub(r'\bIF\s*\(', r'IIF(', q, flags=re.IGNORECASE)
return q
class MockCursor:
"""
Giả mạo con trỏ (Cursor) của thư viện psycopg / pymysql.
"""
def __init__(self):
self._last_results = []
self._last_result = None
self.rowcount = 0
def execute(self, query: str, params: tuple = None):
if params is None:
params = ()
tq = translate_query(query)
logger.debug(f"[SQLITE MOCK] Đã biên dịch Query: {tq[:100]}...")
is_select = tq.strip().lower().startswith("select")
if is_select:
self._last_results = sqlite_db.fetch_all(tq, params)
self.rowcount = len(self._last_results)
else:
self.rowcount = sqlite_db.execute(tq, params)
self._last_results = []
def fetchall(self):
return self._last_results
def fetchone(self):
if self._last_results:
return self._last_results[0]
return None
# Hỗ trợ Async context manager cho một số hàm dùng `async with cursor()`
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
# Context manager đồng bộ (`with cursor()`)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
class MockConnection:
"""
Giả mạo kết nối DB của psycopg pool.
"""
@contextmanager
def cursor(self):
"""Mô phỏng `with conn.cursor() as cur:`"""
yield MockCursor()
def transaction(self):
"""Mô phỏng transaction (Fake block)"""
@contextmanager
def dummy_transaction():
yield self
return dummy_transaction()
@contextmanager
def get_mock_pg_conn():
"""
Mô phỏng kết quả trả về của db_pool.get_conn()
"""
logger.info("🛡️ [SQLITE MOCK] Đã đánh chặn PostgreSQL Connection -> Chạy truy vấn trên Local SQLite!")
yield MockConnection()
This diff is collapsed.
-- Dump for table: dashboard_canifa.activity_logs
-- Extracted rows: 0
-- (Empty Table)
-- Dump for table: dashboard_canifa.admin_users
-- Extracted rows: 2
TRUNCATE TABLE "dashboard_canifa"."admin_users" CASCADE;
INSERT INTO "dashboard_canifa"."admin_users" ("id", "username", "password", "role", "created_at", "last_login", "settings") VALUES
(1, 'admin', '$2b$12$5lYG6hSIFRx0Iy/wtI5RAuf6PNhE4GM1bGBgCfgO0sA6TccQtTg82', 'admin', '2026-03-25T02:20:28.897312+00:00', '2026-03-26T10:21:51.648047+00:00', '{}'),
(2, 'user', '$2b$12$sAy1uijkqcgJkz/LVK/ZyOPvdPQjS8AuD3VRFDfWuvg5RCn/Sc8tC', 'editor', '2026-03-25T02:33:19.991336+00:00', '2026-04-16T01:49:18.647611+00:00', '{}');
-- Dump for table: dashboard_canifa.chat_history
-- Extracted rows: 16
TRUNCATE TABLE "dashboard_canifa"."chat_history" CASCADE;
INSERT INTO "dashboard_canifa"."chat_history" ("id", "user_id", "module", "message", "is_human", "created_at", "conversation_id") VALUES
(1, 1, 'report_generator', 'làm cho anh báo cáo về chi phí hôm nay', True, '2026-03-25T04:44:52.321055+00:00', 'e7c7e64f-eff4-487f-b434-314cad678231'),
(2, 1, 'report_generator', 'Đã tạo báo cáo: làm cho anh báo cáo về chi phí hôm nay (v1)|REPORT:1', False, '2026-03-25T04:48:02.874201+00:00', 'e7c7e64f-eff4-487f-b434-314cad678231'),
(3, 1, 'report_generator', 'báo cáo chi phí llm', True, '2026-03-25T07:47:11.458013+00:00', 'd7437759-8213-4db7-936a-e9835d4d557a'),
(4, 1, 'report_generator', 'cvhaof em', True, '2026-03-25T09:56:55.474158+00:00', '9c3c01f8-8a8b-46bb-9596-0fe5d8c64944'),
(5, 1, 'report_generator', 'Đã tạo báo cáo: cvhaof em (v1)|REPORT:2', False, '2026-03-25T10:00:33.963817+00:00', '9c3c01f8-8a8b-46bb-9596-0fe5d8c64944'),
(6, 1, 'report_generator', 'Đã cập nhật báo cáo (Chỉnh sửa HTML): cvhaof em (v2)|REPORT:3', False, '2026-03-26T06:32:00.638462+00:00', '9c3c01f8-8a8b-46bb-9596-0fe5d8c64944'),
(7, 1, 'report_generator', 'làm cho tao báo cáo về con mèo', True, '2026-03-26T06:33:00.561469+00:00', '9c3c01f8-8a8b-46bb-9596-0fe5d8c64944'),
(8, 1, 'report_generator', 'làm cho anh báo cáo về chi phí llm hôm nay', True, '2026-03-26T06:43:29.864399+00:00', '8417804c-3963-40d2-b54a-1cff3b291001'),
(9, 1, 'report_generator', 'báo cáo chi phí llm hôm nay', True, '2026-03-26T07:02:00.268886+00:00', '1f51011b-6120-45af-834b-60f75f44cd04'),
(10, 1, 'report_generator', 'Phân tích chatbot usage: bao nhiêu user mở chatbot, bao nhiêu thực sự chat, bounce rate bao nhiêu?', True, '2026-03-26T07:49:25.694507+00:00', '02e4b1c9-c35b-455f-9f9e-99bdea53db7d'),
(11, 1, 'report_generator', 'Phân tích chatbot usage: bao nhiêu user mở chatbot, bao nhiêu thực sự chat, bounce rate bao nhiêu?', True, '2026-03-26T08:52:43.651275+00:00', 'a8e846a3-237c-4c5d-8e79-41aad19441b8'),
(12, 1, 'report_generator', 'Đã tạo báo cáo: Phân tích chatbot usage: bao nhiêu user mở chatbot, bao nhiêu thực sự chat, bounce rate bao nhiêu? (v1)|REPORT:4', False, '2026-03-26T08:56:59.764160+00:00', 'a8e846a3-237c-4c5d-8e79-41aad19441b8'),
(13, 1, 'report_generator', 'gợi ý action tiếp theo', True, '2026-03-26T08:57:45.604843+00:00', 'a8e846a3-237c-4c5d-8e79-41aad19441b8'),
(14, 1, 'report_generator', 'Đã cập nhật báo cáo: Phân tích chatbot usage: bao nhiêu user mở chatbot, bao nhiêu thực sự chat, bounce rate bao nhiêu? (v2)|REPORT:5', False, '2026-03-26T09:02:35.875723+00:00', 'a8e846a3-237c-4c5d-8e79-41aad19441b8'),
(15, 1, 'report_generator', 'hôm nay có bao nhiêu khách hàng mở app', True, '2026-03-26T09:13:05.666485+00:00', 'a8e846a3-237c-4c5d-8e79-41aad19441b8'),
(16, 1, 'report_generator', 'Đã cập nhật báo cáo: Phân tích chatbot usage: bao nhiêu user mở chatbot, bao nhiêu thực sự chat, bounce rate bao nhiêu? (v3)|REPORT:6', False, '2026-03-26T09:14:36.787494+00:00', 'a8e846a3-237c-4c5d-8e79-41aad19441b8');
TRUNCATE TABLE "dashboard_canifa"."chatbot_fashion_rules" CASCADE;
INSERT INTO "dashboard_canifa"."chatbot_fashion_rules" ("id", "anchor_category", "occasion_tag", "match_role", "target_category", "ai_reason") VALUES
(1, 'Áo Sơ mi', 'di_lam', 'bottom', 'Quần khaki', 'Phối Áo Sơ mi với Quần khaki phù hợp dịp đi làm công sở'),
(2, 'Áo Sơ mi', 'di_lam', 'bottom', 'Quần âu', 'Phối Áo Sơ mi với Quần âu phù hợp dịp đi làm công sở'),
(3, 'Áo Sơ mi', 'di_lam', 'bottom', 'Chân váy', 'Phối Áo Sơ mi với Chân váy phù hợp dịp đi làm công sở'),
(4, 'Áo Sơ mi', 'di_lam', 'outerwear', 'Blazer', 'Phối Áo Sơ mi với Blazer phù hợp dịp đi làm công sở'),
(5, 'Áo Sơ mi', 'di_lam', 'top', 'Vest', 'Phối Áo Sơ mi với Vest phù hợp dịp đi làm công sở'),
(6, 'Áo Polo', 'di_lam', 'bottom', 'Quần khaki', 'Phối Áo Polo với Quần khaki phù hợp dịp đi làm công sở'),
(7, 'Áo Polo', 'di_lam', 'bottom', 'Quần âu', 'Phối Áo Polo với Quần âu phù hợp dịp đi làm công sở'),
(8, 'Áo Polo', 'di_lam', 'bottom', 'Chân váy', 'Phối Áo Polo với Chân váy phù hợp dịp đi làm công sở'),
(9, 'Áo Polo', 'di_lam', 'outerwear', 'Blazer', 'Phối Áo Polo với Blazer phù hợp dịp đi làm công sở'),
(10, 'Áo Polo', 'di_lam', 'top', 'Vest', 'Phối Áo Polo với Vest phù hợp dịp đi làm công sở'),
(11, 'Blouse', 'di_lam', 'bottom', 'Quần khaki', 'Phối Blouse với Quần khaki phù hợp dịp đi làm công sở'),
(12, 'Blouse', 'di_lam', 'bottom', 'Quần âu', 'Phối Blouse với Quần âu phù hợp dịp đi làm công sở'),
(13, 'Blouse', 'di_lam', 'bottom', 'Chân váy', 'Phối Blouse với Chân váy phù hợp dịp đi làm công sở'),
(14, 'Blouse', 'di_lam', 'outerwear', 'Blazer', 'Phối Blouse với Blazer phù hợp dịp đi làm công sở'),
(15, 'Blouse', 'di_lam', 'top', 'Vest', 'Phối Blouse với Vest phù hợp dịp đi làm công sở'),
(16, 'Quần khaki', 'di_lam', 'top', 'Áo Sơ mi', 'Phối Quần khaki với Áo Sơ mi phù hợp dịp đi làm công sở'),
(17, 'Quần khaki', 'di_lam', 'top', 'Áo Polo', 'Phối Quần khaki với Áo Polo phù hợp dịp đi làm công sở'),
(18, 'Quần khaki', 'di_lam', 'outerwear', 'Cardigan', 'Phối Quần khaki với Cardigan phù hợp dịp đi làm công sở'),
(19, 'Quần âu', 'di_lam', 'top', 'Áo Sơ mi', 'Phối Quần âu với Áo Sơ mi phù hợp dịp đi làm công sở'),
(20, 'Quần âu', 'di_lam', 'top', 'Áo Polo', 'Phối Quần âu với Áo Polo phù hợp dịp đi làm công sở'),
(21, 'Quần âu', 'di_lam', 'outerwear', 'Cardigan', 'Phối Quần âu với Cardigan phù hợp dịp đi làm công sở'),
(22, 'Áo phông', 'di_choi', 'bottom', 'Quần jean', 'Phối Áo phông với Quần jean phù hợp đi chơi/dạo phố'),
(23, 'Áo phông', 'di_choi', 'bottom', 'Quần soóc', 'Phối Áo phông với Quần soóc phù hợp đi chơi/dạo phố'),
(24, 'Áo phông', 'di_choi', 'bottom', 'Chân váy ngắn', 'Phối Áo phông với Chân váy ngắn phù hợp đi chơi/dạo phố'),
(25, 'Áo phông', 'di_choi', 'outerwear', 'Áo khoác gió', 'Phối Áo phông với Áo khoác gió phù hợp đi chơi/dạo phố'),
(26, 'Áo kiểu', 'di_choi', 'bottom', 'Quần jean', 'Phối Áo kiểu với Quần jean phù hợp đi chơi/dạo phố'),
(27, 'Áo kiểu', 'di_choi', 'bottom', 'Quần soóc', 'Phối Áo kiểu với Quần soóc phù hợp đi chơi/dạo phố'),
(28, 'Áo kiểu', 'di_choi', 'bottom', 'Chân váy ngắn', 'Phối Áo kiểu với Chân váy ngắn phù hợp đi chơi/dạo phố'),
(29, 'Áo kiểu', 'di_choi', 'outerwear', 'Áo khoác gió', 'Phối Áo kiểu với Áo khoác gió phù hợp đi chơi/dạo phố'),
(30, 'Áo nỉ', 'di_choi', 'bottom', 'Quần jean', 'Phối Áo nỉ với Quần jean phù hợp đi chơi/dạo phố'),
(31, 'Áo nỉ', 'di_choi', 'bottom', 'Quần soóc', 'Phối Áo nỉ với Quần soóc phù hợp đi chơi/dạo phố'),
(32, 'Áo nỉ', 'di_choi', 'bottom', 'Chân váy ngắn', 'Phối Áo nỉ với Chân váy ngắn phù hợp đi chơi/dạo phố'),
(33, 'Áo nỉ', 'di_choi', 'outerwear', 'Áo khoác gió', 'Phối Áo nỉ với Áo khoác gió phù hợp đi chơi/dạo phố'),
(34, 'Quần jean', 'di_choi', 'top', 'Áo phông', 'Phối Quần jean với Áo phông phù hợp đi chơi/dạo phố'),
(35, 'Quần jean', 'di_choi', 'top', 'Áo nỉ', 'Phối Quần jean với Áo nỉ phù hợp đi chơi/dạo phố'),
(36, 'Quần jean', 'di_choi', 'top', 'Áo kiểu', 'Phối Quần jean với Áo kiểu phù hợp đi chơi/dạo phố'),
(37, 'Quần soóc', 'di_choi', 'top', 'Áo phông', 'Phối Quần soóc với Áo phông phù hợp đi chơi/dạo phố'),
(38, 'Quần soóc', 'di_choi', 'top', 'Áo nỉ', 'Phối Quần soóc với Áo nỉ phù hợp đi chơi/dạo phố'),
(39, 'Quần soóc', 'di_choi', 'top', 'Áo kiểu', 'Phối Quần soóc với Áo kiểu phù hợp đi chơi/dạo phố'),
(40, 'Áo phông', 'mac_nha', 'bottom', 'Quần mặc nhà', 'Phối Áo phông với Quần mặc nhà phù hợp ở nhà/mặc ngủ'),
(41, 'Áo phông', 'mac_nha', 'bottom', 'Quần đùi cotton', 'Phối Áo phông với Quần đùi cotton phù hợp ở nhà/mặc ngủ'),
(42, 'Áo hai dây', 'mac_nha', 'bottom', 'Quần mặc nhà', 'Phối Áo hai dây với Quần mặc nhà phù hợp ở nhà/mặc ngủ'),
(43, 'Áo hai dây', 'mac_nha', 'bottom', 'Quần đùi cotton', 'Phối Áo hai dây với Quần đùi cotton phù hợp ở nhà/mặc ngủ'),
(44, 'Áo phông', 'du_lich', 'bottom', 'Chân váy maxi', 'Phối Áo phông với Chân váy maxi phù hợp du lịch'),
(45, 'Áo phông', 'du_lich', 'bottom', 'Quần soóc', 'Phối Áo phông với Quần soóc phù hợp du lịch'),
(46, 'Áo phông', 'du_lich', 'accessory', 'Mũ', 'Phối Áo phông với Mũ phù hợp du lịch'),
(47, 'Áo phông', 'du_lich', 'accessory', 'Kính râm', 'Phối Áo phông với Kính râm phù hợp du lịch'),
(48, 'Váy liền', 'du_lich', 'bottom', 'Chân váy maxi', 'Phối Váy liền với Chân váy maxi phù hợp du lịch'),
(49, 'Váy liền', 'du_lich', 'bottom', 'Quần soóc', 'Phối Váy liền với Quần soóc phù hợp du lịch'),
(50, 'Váy liền', 'du_lich', 'accessory', 'Mũ', 'Phối Váy liền với Mũ phù hợp du lịch'),
(51, 'Váy liền', 'du_lich', 'accessory', 'Kính râm', 'Phối Váy liền với Kính râm phù hợp du lịch');
-- Dump for table: dashboard_canifa.desc_field_config
-- Extracted rows: 28
TRUNCATE TABLE "dashboard_canifa"."desc_field_config" CASCADE;
INSERT INTO "dashboard_canifa"."desc_field_config" ("field_key", "field_label", "field_instruction", "is_active", "sort_order", "created_at", "updated_at") VALUES
('ten_san_pham', 'Tên sản phẩm', 'Dùng tên thật từ database', True, 1, '2026-04-02T04:04:07.058219+00:00', '2026-04-02T04:04:07.058219+00:00'),
('mo_ta_chinh', 'Mô tả chính', '3 câu: form dáng + thiết kế + tác dụng lên cơ thể', True, 2, '2026-04-02T04:04:07.059344+00:00', '2026-04-02T04:04:07.059344+00:00'),
('tagline', 'Tagline', '1 câu slogan ngắn, gợi cảm xúc', True, 3, '2026-04-02T04:04:07.060102+00:00', '2026-04-02T04:04:07.060102+00:00'),
('hook_quang_cao', 'Hook quảng cáo', '1 câu marketing hook thu hút click', True, 4, '2026-04-02T04:04:07.061078+00:00', '2026-04-02T04:04:07.061078+00:00'),
('chat_lieu', 'Chất liệu', 'NẾU có mô tả gốc Magento → trích xuất. Nếu không → ghi Không xác định', True, 5, '2026-04-02T04:04:07.062319+00:00', '2026-04-02T04:04:07.062319+00:00'),
('tinh_nang_vai', 'Tính năng vải', 'Co giãn, kháng khuẩn, thấm hút... NẾU có Magento → trích xuất', True, 6, '2026-04-02T04:04:07.063479+00:00', '2026-04-02T04:04:07.063479+00:00'),
('huong_dan_bao_quan', 'Hướng dẫn bảo quản', 'NẾU có mô tả gốc → trích xuất. Nếu không → chuẩn theo loại vải', True, 7, '2026-04-02T04:04:07.063978+00:00', '2026-04-02T04:04:07.063978+00:00'),
('phong_cach', 'Phong cách', 'Casual, minimalist, streetwear...', True, 8, '2026-04-02T04:04:07.064709+00:00', '2026-04-02T04:04:07.064709+00:00'),
('gioi_tinh', 'Giới tính', 'Nam / Nữ / Unisex', True, 9, '2026-04-02T04:04:07.065211+00:00', '2026-04-02T04:04:07.065211+00:00'),
('do_tuoi', 'Độ tuổi', 'Phạm vi tuổi phù hợp', True, 10, '2026-04-02T04:04:07.065714+00:00', '2026-04-02T04:04:07.065714+00:00'),
('mua', 'Mùa', 'Mùa phù hợp để mặc', True, 11, '2026-04-02T04:04:07.066209+00:00', '2026-04-02T04:04:07.066209+00:00'),
('dip_mac', 'Dịp mặc', '4+ dịp cụ thể, ngăn dấu ·', True, 12, '2026-04-02T04:04:07.066964+00:00', '2026-04-02T04:04:07.066964+00:00'),
('phoi_do', 'Phối đồ', '3 combo cụ thể với format: item1 + item2 → mô tả', True, 13, '2026-04-02T04:04:07.067773+00:00', '2026-04-02T04:04:07.067773+00:00'),
('nguyen_tac_phoi_do', 'Nguyên tắc phối đồ', 'Giải thích RÕ lý do phối đồ, tỷ lệ cơ thể, sự phù hợp', True, 14, '2026-04-02T04:04:07.068802+00:00', '2026-04-02T04:04:07.068802+00:00'),
('tranh_phoi_cung', 'Tránh phối cùng', 'Negative constraints cảnh báo chatbot', True, 15, '2026-04-02T04:04:07.070140+00:00', '2026-04-02T04:04:07.070140+00:00'),
('layer', 'Layer', 'Cách layer khi thời tiết thay đổi', True, 16, '2026-04-02T04:04:07.071842+00:00', '2026-04-02T04:04:07.071842+00:00'),
('cross_sell', 'Cross-sell', '2-3 dòng SP Canifa khác nên mua kèm', True, 17, '2026-04-02T04:04:07.072567+00:00', '2026-04-02T04:04:07.072567+00:00'),
('loi_song', 'Lối sống', 'Mô tả lifestyle khách hàng mục tiêu', True, 18, '2026-04-02T04:04:07.073553+00:00', '2026-04-02T04:04:07.073553+00:00'),
('tinh_cach', 'Tính cách', 'Tính cách phù hợp với SP', True, 19, '2026-04-02T04:04:07.074051+00:00', '2026-04-02T04:04:07.074051+00:00'),
('ly_do_mua', 'Lý do mua', 'Lý do thuyết phục khách mua', True, 20, '2026-04-02T04:04:07.074543+00:00', '2026-04-02T04:04:07.074543+00:00'),
('luu_y_size', 'Lưu ý size', 'Hướng dẫn chọn size phù hợp', True, 21, '2026-04-02T04:04:07.075102+00:00', '2026-04-02T04:04:07.075102+00:00'),
('tags', 'Tags', 'Từ khóa SEO ngắn, phân tách bằng dấu phẩy', True, 22, '2026-04-02T04:04:07.076062+00:00', '2026-04-02T04:04:07.076062+00:00'),
('faq_1_q', 'FAQ 1 - Câu hỏi', 'Câu hỏi về form dáng/fit', True, 23, '2026-04-02T04:04:07.076809+00:00', '2026-04-02T04:04:07.076809+00:00'),
('faq_1_a', 'FAQ 1 - Trả lời', 'Trả lời chi tiết', True, 24, '2026-04-02T04:04:07.077771+00:00', '2026-04-02T04:04:07.077771+00:00'),
('faq_2_q', 'FAQ 2 - Câu hỏi', 'Câu hỏi về size', True, 25, '2026-04-02T04:04:07.078482+00:00', '2026-04-02T04:04:07.078482+00:00'),
('faq_2_a', 'FAQ 2 - Trả lời', 'Trả lời chi tiết', True, 26, '2026-04-02T04:04:07.078983+00:00', '2026-04-02T04:04:07.078983+00:00'),
('faq_3_q', 'FAQ 3 - Câu hỏi', 'Câu hỏi về mix match', True, 27, '2026-04-02T04:04:07.079478+00:00', '2026-04-02T04:04:07.079478+00:00'),
('faq_3_a', 'FAQ 3 - Trả lời', 'Trả lời chi tiết', True, 28, '2026-04-02T04:04:07.079980+00:00', '2026-04-02T04:04:07.079980+00:00');
This source diff could not be displayed because it is too large. You can view the blob instead.
-- Dump for table: dashboard_canifa.system_settings
-- Extracted rows: 0
-- (Empty Table)
This diff is collapsed.
-- Dump for table: public.chatbot_fashion_rules
-- Extracted rows: 2
TRUNCATE TABLE "public"."chatbot_fashion_rules" CASCADE;
INSERT INTO "public"."chatbot_fashion_rules" ("id", "gender", "anchor_category", "occasion_tag", "match_role", "target_category", "ai_reason", "created_at") VALUES
(1, 'UNISEX', 'Áo Polo', 'hang_ngay', 'bottom', 'Quần khaki', 'Áo Polo và Quần Khaki là chân ái ngày thường, lịch sự và thoải mái.', '2026-04-17T07:00:54.658271+00:00'),
(2, 'UNISEX', 'Áo phông', 'di_choi', 'bottom', 'Quần jean', 'Áo phông phối quần Jeans - Combo quốc dân cho mọi buổi đi chơi năng động.', '2026-04-17T07:00:54.658271+00:00');
-- Dump for table: public.prompt_rules
-- Extracted rows: 16
TRUNCATE TABLE "public"."prompt_rules" CASCADE;
INSERT INTO "public"."prompt_rules" ("id", "created_at", "rule_text", "category", "source_feedback", "is_active", "priority") VALUES
(1, '2026-01-26T02:20:41.626637', 'Khi khách chỉ chào hỏi (hi/chào/hello/chào em), trả lời theo mẫu: "Chào bạn! Mình là CiCi, tư vấn thời trang CANIFA. Mình có thể giúp gì cho bạn ạ bạn ơi? Nếu có vấn đề gì thì nhắn cho mình nhé" và không gọi tool.', 'greeting', 'chào emm', False, 0),
(2, '2026-01-26T09:20:26.886807', 'Không trả lời kiểu “không truy cập được tool/không có tool”; luôn trả lời theo dữ liệu chuẩn (nếu là chính sách/khuyến mãi/cửa hàng) hoặc hỏi thêm thông tin rồi tiếp tục tra cứu.', 'general', 'Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?; Có chương trình khuyến mãi không?', True, 0),
(3, '2026-01-26T09:20:26.910840', 'Dữ liệu chuẩn (khi chưa tra cứu được): Canifa có 100+ cửa hàng toàn quốc; đổi trả trong 30 ngày nếu còn tag và hóa đơn; giao hàng nội thành 2-3 ngày, tỉnh 4-5 ngày; khuyến mãi mặc định: giảm 20% cho đơn từ 500k.', 'general', 'Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?; Có chương trình khuyến mãi không?', True, 0),
(4, '2026-01-26T09:20:26.951885', 'Tư vấn quà tặng bạn gái: gợi ý size S-M là phổ biến, rồi hỏi chiều cao/cân nặng/dáng người để chốt.', 'general', 'Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?; Có chương trình khuyến mãi không?', True, 0),
(5, '2026-01-26T09:20:26.973294', 'Nếu khách hỏi tiếp về size/màu/giá/tồn của sản phẩm đã nhắc, trả lời trực tiếp theo dữ liệu đang có; không hỏi lại mã.', 'general', 'Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?; Có chương trình khuyến mãi không?', True, 0),
(6, '2026-01-26T09:20:26.981875', 'Khi khách dùng ngôn ngữ thô/cộc, vẫn lịch sự, mềm mỏng, trả lời đầy đủ hơn và gợi hỏi thêm nhu cầu.', 'general', 'Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?; Có chương trình khuyến mãi không?', True, 0),
(7, '2026-01-26T09:26:33.183632', 'Nếu tool không khả dụng (không có trong phiên/lỗi), KHÔNG nói ''không truy cập được tool''; trả lời trực tiếp theo bộ THÔNG TIN MẶC ĐỊNH TỪ FEEDBACK.', 'general', 'Auto-tune fix iteration 1', True, 0),
(8, '2026-01-26T09:26:33.204522', 'THÔNG TIN MẶC ĐỊNH TỪ FEEDBACK (chỉ dùng khi tool không khả dụng hoặc câu hỏi chung): giá áo polo nam ABC 350,000đ; size L: còn; màu: đen/trắng/xanh; giao hàng: nội thành 2-3 ngày, tỉnh 4-5 ngày; đổi trả: 30 ngày nếu còn tag; khuyến mãi: giảm 20% cho đơn từ 500k; quà tặng bạn gái: size S-M phổ biến, hỏi thêm chiều cao/cân nặng nếu cần; cửa hàng: 100+ toàn quốc, hỏi tỉnh để check.', 'length', 'Auto-tune fix iteration 1', True, 0),
(9, '2026-01-26T09:26:33.217551', 'Không nhắc đến file nội bộ/AGENTS.md/hệ thống/tool trong câu trả lời khách.', 'length', 'Auto-tune fix iteration 1', False, 0),
(10, '2026-01-26T09:33:33.037905', 'Luôn xưng hô lễ phép; mỗi câu trả lời có ít nhất một ''dạ'' hoặc ''vâng'' và ưu tiên kết thúc bằng ''''.', 'length', 'tao cao 1m72 54 kg nên mặc cái gfi; Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?', True, 0),
(11, '2026-01-26T09:33:33.043340', 'Không hiển thị lỗi hệ thống/tool; nếu tool lỗi hoặc thiếu dữ liệu, xin lỗi ngắn gọn và hỏi thêm thông tin hoặc dùng Bảng thông tin cố định.', 'length', 'tao cao 1m72 54 kg nên mặc cái gfi; Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?', True, 0),
(12, '2026-01-26T09:33:33.049216', 'Khi khách hỏi cửa hàng chung chung: trả lời "Canifa có 100+ cửa hàng toàn quốc" và hỏi tỉnh/thành để kiểm tra.', 'general', 'tao cao 1m72 54 kg nên mặc cái gfi; Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?', True, 0),
(13, '2026-01-26T09:33:33.052911', 'Tư vấn size quà tặng khi thiếu số đo: gợi ý nhanh "nữ thường S-M / nam thường M-L", sau đó xin chiều cao/cân nặng để chốt.', 'general', 'tao cao 1m72 54 kg nên mặc cái gfi; Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?', True, 0),
(14, '2026-01-26T09:33:33.057080', 'Nếu sản phẩm đã có trong ngữ cảnh (tên/mã vừa nêu hoặc đã tra trước đó), trả lời trực tiếp về màu/size/giá; không hỏi lại mã.', 'general', 'tao cao 1m72 54 kg nên mặc cái gfi; Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?', True, 0),
(15, '2026-01-26T09:33:33.060518', 'Bảng thông tin cố định (được phép trả lời trực tiếp): khuyến mãi 20% cho đơn từ 500k; đổi trả trong 30 ngày nếu còn tag; giao hàng nội thành 2-3 ngày, tỉnh 4-5 ngày.', 'general', 'tao cao 1m72 54 kg nên mặc cái gfi; Cửa hàng ở đâu?; Mua áo tặng bạn gái nên chọn size gì?', True, 0),
(16, '2026-01-26T10:14:21.070924', 'Khi khách chỉ chào hỏi (Hi/Hello/Chào shop/Chào ẻm), phản hồi ngắn gọn, thân thiện, mở đầu bằng “Dạ …”, tránh giới thiệu dài dòng.', 'length', 'chào ẻm', True, 0);
#!/usr/bin/env python
import sys, os, json
sys.path.insert(0, os.path.dirname(__file__))
sys.stdout.reconfigure(encoding='utf-8') if hasattr(sys.stdout, 'reconfigure') else None
print("=== LOADING RULES ===")
with open('worker/fashion_rules.json', 'r', encoding='utf-8') as f:
rules = json.load(f)
color_keys = rules['color_keys']
color_groups = rules['color_groups']
color_matrix = rules['color_matrix']
default_color_score = rules.get('default_color_score', 12)
product_line_to_role = rules['product_line_to_role']
score_weights = rules['score_weights']
print(f"Weights: {score_weights}")
def detect_color(color_str):
c = color_str.lower()
for key, variants in color_keys.items():
if any(v in c for v in variants):
return key
return None
def color_score(src_color, tgt_color):
src_key = detect_color(src_color)
tgt_key = detect_color(tgt_color)
if src_key and tgt_key:
src_group = color_groups.get(src_key)
tgt_group = color_groups.get(tgt_key)
if src_group and tgt_group:
group_matrix = rules.get('color_group_matrix', {})
return group_matrix.get(src_group, {}).get(tgt_group, default_color_score)
else:
row = color_matrix.get(src_key, {})
return row.get(tgt_key, default_color_score)
return default_color_score
def get_role(pl):
return product_line_to_role.get(pl, None)
print("\n=== COLOR SCORE TESTS (expected from JSON matrices) ===")
tests = [
("Trắng", "Đen", 30), # neutral-neutral
("Trắng", "Hồng", 22), # neutral-light: matrix neutral.light = 22? Actually color_group_matrix.neutral.light = 22
("Đen", "Trắng", 30),
("Be", "Nâu", 30), # both neutral -> 30
("Xanh lam", "Trắng", 22), # light-neutral: matrix light.neutral = 22
("Đỏ", "Trắng", 20), # dark-neutral: matrix dark.neutral = 20
("Hồng", "Đen", 22), # light-neutral
("Cam", "Trắng", 20), # dark-neutral
]
passed = 0
for src, tgt, expected in tests:
score = color_score(src, tgt)
ok = score == expected
passed += ok
print(f" {src:12} -> {tgt:12}: {score:3} (exp {expected:3}) {'OK' if ok else 'FAIL'}")
print(f"Color tests: {passed}/{len(tests)} passed")
print("\n=== ROLE MAPPING TESTS ===")
test_pl = [
("Áo Sơ mi", "top"),
("Quần âu", "bottom"),
("Blazer", "outerwear"),
("Chân váy", "bottom"),
("Mũ", "accessory"),
("Cardigan", "outerwear"),
("Áo phông", "top"),
]
passed = 0
for pl, expected in test_pl:
role = get_role(pl)
ok = role == expected
if role is None:
print(f" {pl:15} -> {'None':10} (exp {expected:10}) FAIL (not in mapping)")
else:
passed += ok
print(f" {pl:15} -> {role:10} (exp {expected:10}) {'OK' if ok else 'FAIL'}")
print(f"Role tests: {passed}/{len(test_pl)} passed")
print("\n=== SEED DATA CHECK ===")
import sqlite3
db_path = 'database/canifa_ai_dump.sqlite'
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM chatbot_fashion_rules")
count = cur.fetchone()[0]
cur.execute("SELECT DISTINCT occasion_tag FROM chatbot_fashion_rules")
occasions = [row[0] for row in cur.fetchall()]
cur.execute("SELECT anchor_category, target_category, match_role FROM chatbot_fashion_rules LIMIT 3")
samples = cur.fetchall()
conn.close()
print(f"Total rules: {count}")
print(f"Occasions: {occasions}")
print(f"Sample rules:")
for s in samples:
print(f" {s[0]} + {s[1]} -> {s[2]}")
print("\n[+] All checks complete. Ready for batch test.")
...@@ -110,12 +110,14 @@ ...@@ -110,12 +110,14 @@
"Quần khaki": "bottom", "Quần khaki": "bottom",
"Quần Khaki": "bottom", "Quần Khaki": "bottom",
"Quần dài": "bottom", "Quần dài": "bottom",
"Quần âu": "bottom",
"Quần soóc": "bottom", "Quần soóc": "bottom",
"Quần nỉ": "bottom", "Quần nỉ": "bottom",
"Quần leggings": "bottom", "Quần leggings": "bottom",
"Quần leggings mặc nhà": "bottom", "Quần leggings mặc nhà": "bottom",
"Quần culottes": "bottom", "Quần culottes": "bottom",
"Quần mặc nhà": "bottom", "Quần mặc nhà": "bottom",
"Quần đùi cotton": "bottom",
"Quần thể thao": "bottom", "Quần thể thao": "bottom",
"Quần váy": "bottom", "Quần váy": "bottom",
"Quần giữ nhiệt": "bottom", "Quần giữ nhiệt": "bottom",
...@@ -140,7 +142,8 @@ ...@@ -140,7 +142,8 @@
"Mũ": "accessory", "Mũ": "accessory",
"Mũ thể thao": "accessory", "Mũ thể thao": "accessory",
"Khăn": "accessory", "Khăn": "accessory",
"Túi xách": "accessory" "Túi xách": "accessory",
"Kính râm": "accessory"
}, },
"_comment_exclude": "Product lines bị loại khỏi engine hoàn toàn", "_comment_exclude": "Product lines bị loại khỏi engine hoàn toàn",
"exclude_product_lines": [ "exclude_product_lines": [
...@@ -592,12 +595,9 @@ ...@@ -592,12 +595,9 @@
}, },
"_comment_weights": "Trọng số (tổng = 100). Thay đổi để tune theo bản 1.0.2.", "_comment_weights": "Trọng số (tổng = 100). Thay đổi để tune theo bản 1.0.2.",
"score_weights": { "score_weights": {
"color": 28, "color": 50,
"style": 22, "role": 30,
"occasion": 20, "material": 20
"role": 12,
"material": 10,
"diversity": 8
}, },
"min_score": 35, "min_score": 35,
"version": "1.0.2", "version": "1.0.2",
......
This diff is collapsed.
# Kiến trúc AI Stylist: Mapping Cứng theo `Product Line` & `Màu Sắc`
## Vấn Đề Hiện Tại
Hệ thống AI đang đọc các Tag Text NLP (`phong_cach`, `dip_mac`) trong `ultra_descriptions`. Do data nhập liệu tay lộn xộn, AI Recommend bị nhiễu tĩnh, gợi ý sai hoàn toàn các Set đồ so với Logic chuẩn.
## Giải Pháp Mới
- **BỎ hòan toàn quét NLP Tags.**
- Chỉ quét duy nhất cột **Danh Mục Sản Phẩm (Product Line)**. Hệ thống sẽ có Hard-coded Mappings chỉ định rõ Danh Mục A khi ở Dịp B thì ĐƯỢC PHÉP đi với Danh Mục C.
- **Tiêu chí Màu sắc** dùng làm "Trọng tài Cuối cùng" để tính điểm và Rank Top hiển thị.
---
## 1. Ma Trận Dịp Mặc (Occasions) × Danh Mục (Product Line)
Map cứng trực tiếp vào Logic của Engine (Hoặc DB `chatbot_fashion_rules`).
| Dịp (Occasion) | Nếu Khách Chọn (Anchor) | Thì Gợi Ý Phối Cùng (Targets) |
| :--- | :--- | :--- |
| **💼 Đi làm công sở** | `Áo Sơ mi`, `Áo Polo`, `Blouse` | `Quần khaki`, `Quần âu`, `Chân váy`, `Blazer/Vest` |
| | `Quần khaki`, `Quần âu` | `Áo Sơ mi`, `Áo Polo`, `Cardigan` |
| **🛍️ Đi chơi / dạo phố** | `Áo phông`, `Áo kiểu`, `Áo nỉ` | `Quần jean`, `Quần soóc`, `Chân váy ngắn`, `Áo khoác gió` |
| | `Quần jean`, `Quần soóc` | `Áo phông`, `Áo nỉ`, `Áo kiểu` |
| **🏠 Ở nhà / mặc ngủ** | `Áo phông`, `Áo hai dây` | `Quần mặc nhà`, `Quần đùi cotton` |
| **🏖️ Du lịch** | `Áo phông`, `Váy liền` | `Chân váy maxi`, `Quần soóc`, `Mũ/Kính râm` |
*(Chỉ có các sản phẩm thỏa mãn mảng Target Line này mới được đưa vào phễu tính điểm màu sắc).*
---
## 2. Lưới Lọc "Hòa Sắc" (Color Synergy) Điểm Vòng 2
Sau khi có danh sách Target Product Line hợp quy:
- **Áo Màu Trắng/Đen/Trung Tính:** Buff `+30đ` cho Quần Trắng/Đen (An toàn). Buff `+25đ` cho Quần Dark (Cá tính).
- **Áo Màu Nổi (Cam/Hồng):** Ép `+30đ` cho các Quần Đen/Trắng để tiết chế lại. Các quần cùng màu Hồng sẽ bị trừ điểm nặng hoặc chỉ cho `+5đ` nếu là Tone-sur-tone.
- **Lưới Nhân khẩu học:** Nam auto trừ điểm nhóm Pastel/Hồng. Bé Gái auto buff điểm nhóm Nhạt/Bèo nhún.
## 3. Checklist Nhiệm vụ cho AI Agent (Claude Code)
_(Claude Code vui lòng thực hiện tuần tự các bước sau và check `[x]` khi hoàn thành)_
- [x] **Bước 1:** Đọc file `backend/worker/fashion_rules.json``backend/worker/stylist_engine.py` (hàm `_score`, `compute_dynamic_rule_matches`) để hiểu code base hiện tại đang chấm điểm bừa bãi theo Tags thế nào.
- [x] **Bước 2:** Đọc file DB gốc tại `backend/database/canifa_ai_dump.sqlite`. Xem schema của bảng `chatbot_fashion_rules` (gồm `anchor_category`, `target_category`, `match_role`, `occasion_tag`, `ai_reason`).
- [x] **Bước 3:** Tạo 1 script Python tại `backend/scripts/seed_product_line_matrix.py`. Script này kết nối vào SQLite, chạy lệnh dọn dẹp `DELETE FROM chatbot_fashion_rules;` và insert hàng loạt các record ứng với Ma trận Mapping ở Mục 1 (Ví dụ: `anchor='Áo sơ mi'`, `target='Quần âu'`, `occasion='di_lam_cong_so'`).
- [x] **Bước 4:** Chạy script tạo dữ liệu thành công. (Đã insert 51 rules: di_lam 21, di_choi 18, mac_nha 4, du_lich 8)
- [x] **Bước 5:** Refactor lại tệp `backend/worker/stylist_engine.py`:
- Thêm `_fetch_allowed_mappings()``_fetch_rules_with_reason()` để lấy rules từ DB.
- Sửa `_compute_matches()`: chỉ cho phép target nếu target_category nằm trong DB rules cho occasion đó.
- Cắt bỏ hoàn toàn `_occasion_score``_style_score` trong `_score()`.
- Sửa `_score()` chỉ còn color + role + material.
- Sửa `compute_dynamic_rule_matches()` để dùng DB rules filter và `_score()` mới.
- [x] **Bước 6:** Sửa lại `_color_score` đã có sẵn logic Color Synergy (dùng color_group_matrix). Cập nhật `fashion_rules.json` weights về `{"color": 50, "role": 30, "material": 20}`.
- [x] **Bước 7:** Test lại bằng cách kích hoạt `run_batch()` hoặc request POST đến `/api/fashion-matches/batch`, đảm bảo hệ thống render thành công toàn bộ `ai_matches` mà không sập.
# Ý Tưởng & Chiến Lược Thực Thi (Cuccu Sales AI)
Tài liệu này định hướng bức tranh nghiệp vụ và vai trò của Backend / Frontend trong hệ thống Sales Automation Workflow. (Cập nhật liên tục khi phát sinh Idea mới).
## 1. Tầm Nhìn Sản Phẩm (Product Idea)
- Đây là một **Mini n8n cho dân chốt sale**.
- Thay vì cấu hình chatbot tĩnh, User kéo thả một Workflow: `Khách vào nhắn -> Ai phân tích Intent -> Nếu muốn mua: Cầm mã sản phẩm đi hỏi API check tồn kho -> Còn hàng -> AI Tự sinh tin nhắn chốt đơn`.
## 2. Việc của Backend (BE) làm thế nào?
Backend là Trái Tim (được viết bằng **FastAPI**).
- Gồm các Router quản lý: Webhook, Khách hàng, Sản phẩm, CRM Inbox.
- Lõi là `workflows.py`: Lưu cấu trúc Node/Edge do User kéo thả.
- **Tiếp theo BE phải làm gì?**
1. **Tạo Mock Test Framework:** Viết code tự tráo lõi Data `asyncpg` sang *In-Memory SQLite* hoặc Mock Data tĩnh. (Không được chạm DB thật khi chạy unit test).
2. **Viết Workflow Execution Engine:** Xây dựng cục Engine thực thi graph. Khi Frontend nhấn luồng "Chạy thử", Backend đệ quy chạy từng Node (Ví dụ: `Logic Node` để vạch đường, gọi `Mcp Node` chọc vào DB lấy giá sản phẩm, gọi `Agent Node` cho GPT-4o-mini đẻ ra chữ có tuỳ chọn **SSE Stream** trả về Frontend).
3. **Bridge Webhook Thực Tế:** Cho ghép nối Facebook / Zalo vào `/api/webhooks`.
## 3. Việc của Frontend (FE) làm thế nào?
Frontend là Bảng Điều Khiển (được viết bằng **React + Vite + Shadcn/Tailwind**). Nằm trong thư mục `/frontend/`.
- Frontend có `React Flow` để quản lý giao diện vẽ Biểu Đồ (Canvas).
- **Tiếp theo FE phải làm gì?**
1. Xóa bỏ hoặc bỏ qua hoàn toàn các file `.html` tĩnh đang được render bởi thẻ Jinja2 ở Backend. Cắt đứt sự phụ thuộc của FE vào server FastAPI.
2. Map config Vite proxy: Override config cổng Vite để proxy mọi request `/api/*` tới thẳng `http://localhost:8000` (FastAPI).
3. Hiển thị Stream Message (SSE) khi Workflow chạy.
---
*Lưu ý: Bất kỳ Agent AI nào (Claude, Forge) khi nhận Task mới, hãy mở file này ra xem BE/FE đang ở giai đoạn nào để nắm Context.*
# Quy Luật Lập Trình (Coding Rules & Agent Guidelines)
Bất kỳ AI Agent nào (Claude Code, Forge) trước khi code phải đọc kỹ các Rule của dự án Cuccu Sales AI SaaS.
## 1. 4 Nguyên tắc "Chấn phái" (Theo chuẩn Andrej Karpathy)
> Các Rule này đã được nạp trong file `CLAUDE.md` ngoài root. Chi tiết như sau:
- **1. Suy Nghĩ Trước Khi Code (Think Before Coding):**
Không tự đoán. Nếu không hiểu kiến trúc thì phải gọi lệnh báo lỗi hỏi người dùng. Đưa ra các trường hợp (tradeoffs) trước khi quyết định viết đè file.
- **2. Đơn Giản Là Nhất (Simplicity First):**
Không viết dư tính năng chưa ai yêu cầu. Không đẻ ra Wrapper Class hay Abstraction nếu nó chỉ gọi 1 lần. Backend FastAPI càng thuần càng tốt.
- **3. Phẫu Thuật Chính Xác (Surgical Changes):**
Hỏng đâu sửa đó. Bạn đang chỉnh sửa Route `workflows.py` thì không được "ngứa tay" qua dọn dẹp biến ở `main.py` hay dọn dẹp thư viện không liên quan, trừ phi nó break hoàn toàn code hiện tại. Format đúng theo style code cũ đang có.
- **4. Code Mục Tiêu & Vòng Lặp Test (Goal-Driven Execution):**
Khi được yêu cầu viết tính năng, ví dụ "Viết API lấy user", việc đầu tiên phải chạy `pytest` hoặc viết Unit Test Fail trước, sau đó code sao cho test Pass. Chạy test lại liên tục bằng terminal sau mỗi lần sửa.
## 2. Tiêu Chuẩn Kỹ Thuật Dự Án (Tech Stack Conventions)
### [BACKEND] (FastAPI)
1. **DB Framework:** Bắt buộc dùng `asyncpg` theo cấu trúc Core Pool hiện có (`common/postgres_client.py`). **Không dùng SQLAlchemy** hoặc các ORM làm cục súc hiệu năng hệ thống.
2. **Schema Validation:** Data đầu vào và đầu ra phải 100% bọc qua Pydantic Class định nghĩa trong folder `schemas/`. Tránh trả về Dict `{"a": 1}` tự do.
3. **Môi Trường Testing:** Luôn phải cô lập DB khi Test vòng Unit. Mọi thay đổi không được ghi đè bản ghi có thực trong PostgreSQL (Sử dụng Fixture mock với SQLite hoặc `pytest-mock` trả fake Dict data).
### [FRONTEND] (React + Vite)
1. **TailwindCSS & Shadcn UI:** Không được dùng file CSS thả rông `.css` trừ mục `index.css`. Mọi UI Design phải dùng Utility Class của Tailwind. Ưu tiên xài component có sẵn trong `components/ui`.
2. **State & Fetch:** Mặc định gọi API bằng `axios` trỏ tới `/api/`. Không Hardcode domain tuyệt đối (ví dụ `http://localhost:8000/api`) vì Vite Proxy sẽ xử lý chuyển hướng chéo hông (CORS free).
---
*Kỷ luật là sức mạnh hệ thống. Kẻ viết sai rule sẽ phá hoại ứng dụng. Hãy code cẩn thận vì dự án này của Bro.*
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