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

Update stylist graph, fix classifier logic and push sqlite db

parent d6a0dabb
...@@ -77,3 +77,6 @@ backend/tests/sheet_info.json ...@@ -77,3 +77,6 @@ backend/tests/sheet_info.json
backend/tests/test_n8n_api_output.txt backend/tests/test_n8n_api_output.txt
backend/n8n_result.json backend/n8n_result.json
diff_*.txt diff_*.txt
*.sqlite
*.db
Requirement already satisfied: python-socketio in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (5.13.0)
Requirement already satisfied: bidict>=0.21.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-socketio) (0.23.1)
Requirement already satisfied: python-engineio>=4.11.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-socketio) (4.12.2)
Requirement already satisfied: simple-websocket>=0.10.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-engineio>=4.11.0->python-socketio) (1.1.0)
Requirement already satisfied: wsproto in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from simple-websocket>=0.10.0->python-engineio>=4.11.0->python-socketio) (1.2.0)
Requirement already satisfied: h11<1,>=0.9.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from wsproto->simple-websocket>=0.10.0->python-engineio>=4.11.0->python-socketio) (0.16.0)
...@@ -119,9 +119,41 @@ async def classifier_node(state: StylistProState, config: RunnableConfig): ...@@ -119,9 +119,41 @@ async def classifier_node(state: StylistProState, config: RunnableConfig):
elif output.tool_args: elif output.tool_args:
tool_args = output.tool_args tool_args = output.tool_args
# EXECUTE TOOL INLINE
if tool_name and tool_args is not None:
from agent.tools.get_tools import get_all_tools
all_tools = get_all_tools()
# Fallback handle name 'lead_search_tool' -> 'data_retrieval_tool'
search_name = "data_retrieval_tool" if tool_name == "lead_search_tool" else tool_name
target_tool = next((t for t in all_tools if t.name == search_name), None)
if target_tool:
try:
logger.info(f"🚀 Executing inline tool: {search_name}")
# Some tools might be sync or async, checking for ainvoke support
if hasattr(target_tool, "ainvoke"):
tool_res_str = await target_tool.ainvoke(tool_args, config=config)
else:
tool_res_str = target_tool.invoke(tool_args)
tool_result = tool_res_str
diagnostics.append({
"step": "inline_tool",
"label": f"🛠️ Executed {search_name}",
"content": f"Result length: {len(str(tool_res_str))}",
"elapsed_ms": 0
})
except Exception as e:
logger.error(f"❌ Inline tool error: {e}")
tool_result = json.dumps({"status": "error", "message": str(e), "products": []})
else:
logger.warning(f"⚠️ Tool {search_name} not found in get_all_tools()")
return { return {
"tool_name_used": tool_name, "tool_name_used": tool_name,
"tool_args": tool_args, "tool_args": tool_args,
"tool_result": tool_result,
"early_exit": tool_name is None and output.ai_response is not None, "early_exit": tool_name is None and output.ai_response is not None,
"product_ids": output.product_ids, "product_ids": output.product_ids,
"messages": [AIMessage(content=output.ai_response)] if output.ai_response else [], "messages": [AIMessage(content=output.ai_response)] if output.ai_response else [],
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
- Trộn mô tả trong câu văn (không phải dòng nào cũng có dấu "→") - Trộn mô tả trong câu văn (không phải dòng nào cũng có dấu "→")
- Đổi thứ tự: đôi khi nói cảm nhận trước, rồi mới nêu SKU/giá - Đổi thứ tự: đôi khi nói cảm nhận trước, rồi mới nêu SKU/giá
- **Vary sentence structure**: lúc ngắn gọn, lúc giàu cảm xúc; tránh lặp y hệt emoji/pattern. - **Vary sentence structure**: lúc ngắn gọn, lúc giàu cảm xúc; tránh lặp y hệt emoji/pattern.
- **Giữ số lượng item hợp lý** (2–4) để tránh dài dòng; nếu nhiều hơn, nhóm theo nhu cầu/đối tượng. - **Giữ số lượng item hợp lý** (3–6) để tránh dài dòng; nếu nhiều hơn, nhóm theo nhu cầu/đối tượng.
- **Khi nói về size: PHẢI LIỆT KÊ TỪNG SIZE** — KHÔNG được gộp, KHÔNG dùng "đủ", KHÔNG dùng "từ...đến...": - **Khi nói về size: PHẢI LIỆT KÊ TỪNG SIZE** — KHÔNG được gộp, KHÔNG dùng "đủ", KHÔNG dùng "từ...đến...":
- ❌ SAI: "Size có đủ S, M, L, XL cho chị thoải mái chọn" - ❌ SAI: "Size có đủ S, M, L, XL cho chị thoải mái chọn"
- ❌ SAI: "Có size từ XS đến XL thoải mái chọn" - ❌ SAI: "Có size từ XS đến XL thoải mái chọn"
...@@ -376,37 +376,6 @@ Phối quần jeans là đủ trendy cho cafe rồi! ...@@ -376,37 +376,6 @@ Phối quần jeans là đủ trendy cho cafe rồi!
Mẫu cardigan len mỏng bên dưới đang sale, phối áo thun trắng là chuẩn! 😊" Mẫu cardigan len mỏng bên dưới đang sale, phối áo thun trắng là chuẩn! 😊"
``` ```
### 💰 QUY TẮC HIỂN THỊ GIÁ (BẮT BUỘC):
**LUÔN đọc đúng giá từ dữ liệu tool trả về:**
- `price`: Giá gốc
- `sale_price`: Giá đang bán (sau giảm giá)
**FORMAT GIÁ CHUẨN:**
```
❌ SAI: "299k" (khi sale_price là 149k)
❌ SAI: "giá 299k đang sale" (mơ hồ, không rõ giá sale bao nhiêu)
✅ ĐÚNG (Có sale): "~~299k~~ → 149k (SALE 50%!)"
✅ ĐÚNG (Có sale): "Giá gốc 299k, còn 149k thôi!"
✅ ĐÚNG (Có sale): "149k (giảm từ 299k)"
✅ ĐÚNG (Không sale): "299k"
```
**QUY TẮC:**
1. **NẾU sale_price < price** → Phải hiển thị CẢ HAI giá và nhấn mạnh SALE
2. **NẾU sale_price = price** → Chỉ hiển thị 1 giá
3. **LUÔN dùng giá từ tool** → KHÔNG được tự bịa giá
4. **Làm tròn đẹp:** 149500 → "149k", 244300 → "244k"
**VÍ DỤ CHUẨN:**
```
🌸 [1DS25W006]: Váy liền bé gái hoạ tiết
→ Giá gốc 299k, còn 149k thôi! SALE SỐC 50%! 🔥
→ Prices...
```
**VÍ DỤ VĂN PHONG ĐÚNG:** **VÍ DỤ VĂN PHONG ĐÚNG:**
``` ```
❌ SAI (Dài dòng): "[8TS24W001]: Áo thun cotton basic - 250k, chất vải mát, form regular dễ mặc, size S, M, L, XL" ❌ SAI (Dài dòng): "[8TS24W001]: Áo thun cotton basic - 250k, chất vải mát, form regular dễ mặc, size S, M, L, XL"
......
...@@ -13,7 +13,7 @@ Nhiệm vụ duy nhất: Phân tích intent khách hàng và quyết định l ...@@ -13,7 +13,7 @@ Nhiệm vụ duy nhất: Phân tích intent khách hàng và quyết định l
</system_role> </system_role>
<tool_inventory> <tool_inventory>
1. `lead_search_tool`: Tìm kiếm sản phẩm (Sử dụng cho 90% trường hợp tìm đồ, combo, giá, màu). 1. `data_retrieval_tool`: Tìm kiếm sản phẩm (Sử dụng cho 90% trường hợp tìm đồ, combo, giá, màu).
2. `check_is_stock`: Kiểm tra size, tồn kho, giá cụ thể của 1 SKU. 2. `check_is_stock`: Kiểm tra size, tồn kho, giá cụ thể của 1 SKU.
3. `canifa_store_search`: Tìm địa chỉ cửa hàng. 3. `canifa_store_search`: Tìm địa chỉ cửa hàng.
4. `canifa_get_promotions`: Tra cứu các chương trình khuyến mãi hiện có. 4. `canifa_get_promotions`: Tra cứu các chương trình khuyến mãi hiện có.
...@@ -21,7 +21,7 @@ Nhiệm vụ duy nhất: Phân tích intent khách hàng và quyết định l ...@@ -21,7 +21,7 @@ Nhiệm vụ duy nhất: Phân tích intent khách hàng và quyết định l
</tool_inventory> </tool_inventory>
<search_philosophy_dual_lane> <search_philosophy_dual_lane>
Khi dùng `lead_search_tool`, bạn BẮT BUỘC phân tách: Khi dùng `data_retrieval_tool`, bạn BẮT BUỘC phân tách:
- **Lane 1 (Literal)**: Trích xuất raw_text. Bỏ từ thừa (tìm, mua, cái...), sửa lỗi typo (nghọ nghiêng -> ngọ nghiêng). - **Lane 1 (Literal)**: Trích xuất raw_text. Bỏ từ thừa (tìm, mua, cái...), sửa lỗi typo (nghọ nghiêng -> ngọ nghiêng).
- **Lane 2 (Inferred)**: Suy luận SQL Filters (product_line_vn, gender_target, age_group, tags, keywords). - **Lane 2 (Inferred)**: Suy luận SQL Filters (product_line_vn, gender_target, age_group, tags, keywords).
</search_philosophy_dual_lane> </search_philosophy_dual_lane>
...@@ -36,9 +36,17 @@ Khi dùng `lead_search_tool`, bạn BẮT BUỘC phân tách: ...@@ -36,9 +36,17 @@ Khi dùng `lead_search_tool`, bạn BẮT BUỘC phân tách:
1. Thời tiết: "Trời mưa" -> Áo khoác gió, trượt nước. "Nắng nóng" -> Áo chống nắng, UV. 1. Thời tiết: "Trời mưa" -> Áo khoác gió, trượt nước. "Nắng nóng" -> Áo chống nắng, UV.
2. Dịp: "Đi tiệc" -> Váy liền, Áo kiểu, Sang trọng. "Đi làm" -> Áo sơ mi, Polo, Quần Khaki (Adult). 2. Dịp: "Đi tiệc" -> Váy liền, Áo kiểu, Sang trọng. "Đi làm" -> Áo sơ mi, Polo, Quần Khaki (Adult).
3. Đồ lót: "Sịp/chip" -> Quần lót, Áo lót. (Tool tự tìm gender, bạn chỉ cần set product_line). 3. Đồ lót: "Sịp/chip" -> Quần lót, Áo lót. (Tool tự tìm gender, bạn chỉ cần set product_line).
4. Combo: Khách hỏi "đồ", "set", "phối" -> BẮT BUỘC gọi lead_search_tool. RESET product_line cũ từ insight. 4. Combo: Khách hỏi "đồ", "set", "phối" -> BẮT BUỘC gọi data_retrieval_tool. RESET product_line cũ từ insight.
</mapping_logic_rules> </mapping_logic_rules>
<tool_routing_rules>
- **data_retrieval_tool**: Tìm kiếm sản phẩm chung. ĐẶC BIỆT KHI KHÁCH CHO SKU (VD: "6DS25S012", "[6TO24S010]") -> GỌI NGAY tool này bằng `magento_ref_code`.
- **check_is_stock**: GỌI KHI KHÁCH HỎI TỒN KHO ONLINE (VD: "còn hàng không", "còn size M không", "mã này hết chưa"). NẾU khách hỏi MÃ SKU + "còn không" -> BẮT BUỘC gọi SONG SONG `data_retrieval_tool` và `check_is_stock`. KHÔNG dùng check_is_stock cho tồn kho offline cửa hàng.
- **canifa_store_search**: GỌI KHI HỎI ĐỊA CHỈ (VD: "cửa hàng ở đâu", "Canifa Chùa Bộc").
- **canifa_get_promotions**: GỌI KHI HỎI SALE (VD: "có khuyến mãi gì", "đang có sale gì").
- **canifa_knowledge_search**: GỌI KHI HỎI CHÍNH SÁCH/THẺ VIP (VD: "hạng Gold được gì", "phí ship", "bảng size", "đổi trả").
</tool_routing_rules>
<inheritance_rules> <inheritance_rules>
- Kế thừa Giới tính/Đối tượng từ <user_insight> (VD: Target là Nam -> gender_target="men"). - Kế thừa Giới tính/Đối tượng từ <user_insight> (VD: Target là Nam -> gender_target="men").
- RESET hoàn toàn product_line_vn theo câu hỏi mới nhất. - RESET hoàn toàn product_line_vn theo câu hỏi mới nhất.
...@@ -57,9 +65,9 @@ Trả về DUY NHẤT JSON: ...@@ -57,9 +65,9 @@ Trả về DUY NHẤT JSON:
{ {
"reasoning": "giải thích ngắn gọn tại sao chọn tool này hoặc tại sao chặn", "reasoning": "giải thích ngắn gọn tại sao chọn tool này hoặc tại sao chặn",
"tool_name": "tên_tool" hoặc null, "tool_name": "tên_tool" hoặc null,
"search_args": { "search_args": {
"literal": {"raw_text": "..."}, "literal": {"raw_text": "..."},
"inferred": {"product_line_vn": [...], "gender_target": "...", "tags": [...], "keywords": [...], ...} "inferred": {"product_line_vn": [...], "gender_target": "...", "tags": [...], "keywords": [...], ...}
}, },
"ai_response": "chào hỏi xã giao HOẶC từ chối off-topic (chỉ dùng khi tool_name=null)", "ai_response": "chào hỏi xã giao HOẶC từ chối off-topic (chỉ dùng khi tool_name=null)",
"product_ids": [] "product_ids": []
...@@ -67,6 +75,9 @@ Trả về DUY NHẤT JSON: ...@@ -67,6 +75,9 @@ Trả về DUY NHẤT JSON:
</output_format> </output_format>
""" """
# ============================================================================== # ==============================================================================
# 2. STYLIST PROMPT (The Expert) # 2. STYLIST PROMPT (The Expert)
# ============================================================================== # ==============================================================================
...@@ -77,7 +88,7 @@ Bạn là Chuyên gia Thời trang (Stylist Pro) của CANIFA. Bạn tư vấn d ...@@ -77,7 +88,7 @@ Bạn là Chuyên gia Thời trang (Stylist Pro) của CANIFA. Bạn tư vấn d
<styling_philosophy> <styling_philosophy>
1. GỢI Ý NGAY, KHÔNG HỎI LẠI: Luôn đưa ra lựa chọn. Nếu chưa biết giới tính, gợi ý cả 2 phương án. 1. GỢI Ý NGAY, KHÔNG HỎI LẠI: Luôn đưa ra lựa chọn. Nếu chưa biết giới tính, gợi ý cả 2 phương án.
2. COMBO / OUTFIT (CRITICAL): 2. COMBO / OUTFIT (CRITICAL):
- Nếu khách hỏi "đồ", "set", "phối", "mặc gì" -> PHẢI tạo outfit hoàn chỉnh (Áo + Quần/Váy). - Nếu khách hỏi "đồ", "set", "phối", "mặc gì" -> PHẢI tạo outfit hoàn chỉnh (Áo + Quần/Váy).
- Sử dụng `outfit_recommendations` từ tool result để bốc đúng món phối. - Sử dụng `outfit_recommendations` từ tool result để bốc đúng món phối.
- BẮT BUỘC nhét SKU món phối vào `product_ids`. - BẮT BUỘC nhét SKU món phối vào `product_ids`.
......
...@@ -14,6 +14,8 @@ from datetime import datetime ...@@ -14,6 +14,8 @@ from datetime import datetime
from langfuse import Langfuse from langfuse import Langfuse
from config import USE_LANGFUSE_PROMPTS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
LANGFUSE_SYSTEM_PROMPT_NAME = "canifa-stylist-system-prompt" LANGFUSE_SYSTEM_PROMPT_NAME = "canifa-stylist-system-prompt"
...@@ -41,38 +43,66 @@ def _get_langfuse() -> Langfuse: ...@@ -41,38 +43,66 @@ def _get_langfuse() -> Langfuse:
def get_system_prompt() -> str: def get_system_prompt() -> str:
"""System prompt với ngày hiện tại đã inject.""" """System prompt với ngày hiện tại đã inject."""
if not USE_LANGFUSE_PROMPTS:
logger.info("ℹ️ Using local system prompt fallback (USE_LANGFUSE_PROMPTS=False)")
# Cần fallback system prompt ở đây nếu có local
# Tạm thời trả về rỗng, app sẽ crash hoặc báo lỗi logic nếu không có
return ""
lf = _get_langfuse() lf = _get_langfuse()
prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=CACHE_TTL) try:
return prompt.compile(date_str=datetime.now().strftime("%d/%m/%Y")) prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=CACHE_TTL)
return prompt.compile(date_str=datetime.now().strftime("%d/%m/%Y"))
except Exception as e:
logger.warning(f"⚠️ Failed to fetch system prompt from Langfuse: {e}. Using local fallback.")
return ""
def get_system_prompt_template() -> str: def get_system_prompt_template() -> str:
"""Template chưa replace date_str — dùng cho ChatPromptTemplate. """Template chưa replace date_str — dùng cho ChatPromptTemplate."""
if not USE_LANGFUSE_PROMPTS:
logger.info("ℹ️ Using local system prompt template (USE_LANGFUSE_PROMPTS=False)")
# Hiện tại agent/prompt_module/stylist_pro_prompts.py chứa các prompt local
try:
from agent.prompt_module.stylist_pro_prompts import STYLIST_PRO_RESPONDER_PROMPT
return STYLIST_PRO_RESPONDER_PROMPT
except ImportError:
return ""
Langfuse SDK `.prompt` un-escapes {{ → { for non-variable content,
but LangChain ChatPromptTemplate needs {{ for literal braces.
So we re-escape ALL { } first, then convert only {{date_str}} → {date_str}.
"""
lf = _get_langfuse() lf = _get_langfuse()
prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=CACHE_TTL) try:
# 1) Re-escape all curly braces for LangChain (literal { → {{, } → }}) prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=CACHE_TTL)
raw = prompt.prompt.replace("{", "{{").replace("}", "}}") raw = prompt.prompt.replace("{", "{{").replace("}", "}}")
# 2) Convert only the date_str variable back to LangChain format return raw.replace("{{{{date_str}}}}", "{date_str}")
# After step 1, {{date_str}} became {{{{date_str}}}} → convert to {date_str} except Exception as e:
return raw.replace("{{{{date_str}}}}", "{date_str}") logger.warning(f"⚠️ Failed to fetch system prompt template from Langfuse: {e}. Using local fallback.")
try:
from agent.prompt_module.stylist_pro_prompts import STYLIST_PRO_RESPONDER_PROMPT
return STYLIST_PRO_RESPONDER_PROMPT
except ImportError:
return ""
def read_tool_prompt(filename: str, default_prompt: str = "") -> str: def read_tool_prompt(filename: str, default_prompt: str = "") -> str:
"""Read tool prompt from Langfuse.""" """Read tool prompt from Langfuse."""
name_key = filename.replace(".txt", "") name_key = filename.replace(".txt", "")
if not USE_LANGFUSE_PROMPTS:
logger.info(f"ℹ️ Using local tool prompt for '{name_key}' (USE_LANGFUSE_PROMPTS=False)")
return default_prompt
langfuse_name = LANGFUSE_TOOL_PROMPT_MAP.get(name_key) langfuse_name = LANGFUSE_TOOL_PROMPT_MAP.get(name_key)
if not langfuse_name: if not langfuse_name:
logger.warning(f"⚠️ No Langfuse mapping for tool prompt '{name_key}'") logger.warning(f"⚠️ No Langfuse mapping for tool prompt '{name_key}'")
return default_prompt return default_prompt
lf = _get_langfuse() lf = _get_langfuse()
prompt = lf.get_prompt(langfuse_name, label="production", cache_ttl_seconds=CACHE_TTL) try:
return prompt.prompt prompt = lf.get_prompt(langfuse_name, label="production", cache_ttl_seconds=CACHE_TTL)
return prompt.prompt
except Exception as e:
logger.warning(f"⚠️ Failed to fetch tool prompt '{langfuse_name}' from Langfuse: {e}. Falling back to default.")
return default_prompt
def write_tool_prompt(filename: str, content: str) -> bool: def write_tool_prompt(filename: str, content: str) -> bool:
......
...@@ -14,7 +14,7 @@ from backend.agent.nodes.stylist_node import stylist_node ...@@ -14,7 +14,7 @@ from backend.agent.nodes.stylist_node import stylist_node
from backend.agent.nodes.utils import _extract_text from backend.agent.nodes.utils import _extract_text
# StarRocks connection for Enrichment # StarRocks connection for Enrichment
from agent.lead_stage_agent.product_search_engine import SELECT_COLUMNS, TABLE_NAME from agent.tools.tool_module.search_engine import SELECT_COLUMNS, TABLE_NAME
from common.starrocks_connection import get_db_connection from common.starrocks_connection import get_db_connection
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -131,14 +131,18 @@ class StylistProGraph: ...@@ -131,14 +131,18 @@ class StylistProGraph:
if missing_ids: if missing_ids:
try: try:
db = get_db_connection() from agent.tools.tool_module.db_connector import DBConnector, strip_local_columns
use_sqlite = config.get("configurable", {}).get("db_source") == "sqlite"
db = DBConnector(use_sqlite=use_sqlite)
skus = list(dict.fromkeys(missing_ids)) skus = list(dict.fromkeys(missing_ids))
base_skus = list(dict.fromkeys([s.split("-")[0] for s in skus])) base_skus = list(dict.fromkeys([s.split("-")[0] for s in skus]))
keys = list(dict.fromkeys(skus + base_skus)) keys = list(dict.fromkeys(skus + base_skus))
placeholders = ",".join(["%s"] * len(keys)) placeholders = ",".join(["%s"] * len(keys))
sql = f"SELECT {SELECT_COLUMNS} FROM {TABLE_NAME} WHERE UPPER(magento_ref_code) IN ({placeholders}) OR UPPER(internal_ref_code) IN ({placeholders}) LIMIT 200" safe_columns = strip_local_columns(SELECT_COLUMNS)
rows = await db.execute_query_async(sql, params=tuple(keys + keys)) sql = f"SELECT {safe_columns} FROM {TABLE_NAME} WHERE UPPER(magento_ref_code) IN ({placeholders}) OR UPPER(internal_ref_code) IN ({placeholders}) LIMIT 200"
rows = await db.execute_query(sql, params=tuple(keys + keys))
for r in rows or []: for r in rows or []:
card = { card = {
...@@ -169,6 +173,8 @@ class StylistProGraph: ...@@ -169,6 +173,8 @@ class StylistProGraph:
"updated_insight": result.get("updated_insight"), "updated_insight": result.get("updated_insight"),
"product_ids": ai_product_ids, "product_ids": ai_product_ids,
"products": final_products, "products": final_products,
"tool_name_used": result.get("tool_name_used"),
"tool_result": result.get("tool_result"),
"pipeline": result.get("diagnostics", []) "pipeline": result.get("diagnostics", [])
} }
......
...@@ -41,10 +41,29 @@ def translate_query(query: str) -> str: ...@@ -41,10 +41,29 @@ def translate_query(query: str) -> str:
q, flags=re.IGNORECASE q, flags=re.IGNORECASE
) )
# 5. Map description_text_full to description_data_full for SQLite
q = re.sub(
r'\bdescription_text_full\b',
r'description_data_full AS description_text_full',
q
)
logger.debug(f"📝 [SQLite Translated] {q[:200]}") logger.debug(f"📝 [SQLite Translated] {q[:200]}")
return q return q
def strip_local_columns(query: str) -> str:
"""
Loại bỏ các cột chỉ có ở local SQLite để chạy mượt trên StarRocks Production.
"""
q = query
for col in ["tags,", "similar_items,", "outfit_recommendations,"]:
q = q.replace(col, "")
# Remove any extra spaces left behind
q = re.sub(r',\s*,', ',', q)
return q
class DBConnector: class DBConnector:
""" """
Bộ điều phối DB: StarRocks vs SQLite. Bộ điều phối DB: StarRocks vs SQLite.
...@@ -62,13 +81,15 @@ class DBConnector: ...@@ -62,13 +81,15 @@ class DBConnector:
logger.error(f"❌ [DBConnector] SQLite Error: {e}") logger.error(f"❌ [DBConnector] SQLite Error: {e}")
return [] return []
else: else:
# Strip local columns before sending to StarRocks
sr_query = strip_local_columns(query)
logger.info(f"🚀 [DBConnector] Using StarRocks Production") logger.info(f"🚀 [DBConnector] Using StarRocks Production")
db = get_db_connection() db = get_db_connection()
if not db: if not db:
logger.error("❌ [DBConnector] StarRocks Connection Failed!") logger.error("❌ [DBConnector] StarRocks Connection Failed!")
return [] return []
try: try:
return await db.execute_query_async(query, params) return await db.execute_query_async(sr_query, params)
except Exception as e: except Exception as e:
logger.error(f"❌ [DBConnector] StarRocks Error: {e}. Falling back to SQLite...") logger.error(f"❌ [DBConnector] StarRocks Error: {e}. Falling back to SQLite...")
# Tự động cứu bồ # Tự động cứu bồ
......
...@@ -9,7 +9,6 @@ from pydantic import BaseModel, Field ...@@ -9,7 +9,6 @@ from pydantic import BaseModel, Field
# Core module imports từ local tool_module # Core module imports từ local tool_module
from .db_connector import DBConnector from .db_connector import DBConnector
from .stock_provider import enrich_with_stock
from .product_mapping import get_related_lines, resolve_product_line from .product_mapping import get_related_lines, resolve_product_line
from .pattern_detector import HardPatternDetector from .pattern_detector import HardPatternDetector
from .size_message_builder import build_size_message from .size_message_builder import build_size_message
...@@ -176,7 +175,7 @@ def _build_fixed_clauses(inf: InferredSearch, params: list) -> list[str]: ...@@ -176,7 +175,7 @@ def _build_fixed_clauses(inf: InferredSearch, params: list) -> list[str]:
age = "adult" age = "adult"
params.append(age) params.append(age)
clauses.append("age_by_product = %s") clauses.append("age_by_product = %s")
if inf.master_color: if inf.master_color:
# ── Robust Color Filter ── # ── Robust Color Filter ──
# AI có thể output "Màu đen", "đen", "black" — DB lưu "Đen/ Black" # AI có thể output "Màu đen", "đen", "black" — DB lưu "Đen/ Black"
...@@ -223,7 +222,7 @@ def _build_fixed_clauses(inf: InferredSearch, params: list) -> list[str]: ...@@ -223,7 +222,7 @@ def _build_fixed_clauses(inf: InferredSearch, params: list) -> list[str]:
params.append(inf.price_min); clauses.append("sale_price >= %s") params.append(inf.price_min); clauses.append("sale_price >= %s")
if inf.price_max is not None: if inf.price_max is not None:
params.append(inf.price_max); clauses.append("sale_price <= %s") params.append(inf.price_max); clauses.append("sale_price <= %s")
if inf.size: if inf.size:
params.append(inf.size.upper()) params.append(inf.size.upper())
clauses.append("FIND_IN_SET(%s, REPLACE(size_scale, '|', ',')) > 0") clauses.append("FIND_IN_SET(%s, REPLACE(size_scale, '|', ',')) > 0")
...@@ -266,14 +265,14 @@ class SearchEngine: ...@@ -266,14 +265,14 @@ class SearchEngine:
for w in words: for w in words:
params_lit.extend([f"%{w}%", f"%{w}%"]) params_lit.extend([f"%{w}%", f"%{w}%"])
clauses_lit.append("(LOWER(product_name) LIKE %s OR LOWER(description_text) LIKE %s)") clauses_lit.append("(LOWER(product_name) LIKE %s OR LOWER(description_text) LIKE %s)")
# Áp dụng cứng Gender/Age # Áp dụng cứng Gender/Age
where_lit = " AND ".join(clauses_lit) where_lit = " AND ".join(clauses_lit)
if inf.gender_by_product: if inf.gender_by_product:
params_lit.append(inf.gender_by_product); where_lit += " AND gender_by_product = %s" params_lit.append(inf.gender_by_product); where_lit += " AND gender_by_product = %s"
if inf.age_by_product: if inf.age_by_product:
params_lit.append(inf.age_by_product); where_lit += " AND age_by_product = %s" params_lit.append(inf.age_by_product); where_lit += " AND age_by_product = %s"
sql_lit = f"SELECT {SELECT_COLUMNS} FROM {TABLE_NAME} WHERE {where_lit} ORDER BY quantity_sold DESC LIMIT 50" sql_lit = f"SELECT {SELECT_COLUMNS} FROM {TABLE_NAME} WHERE {where_lit} ORDER BY quantity_sold DESC LIMIT 50"
literal_products = await self.db.execute_query(sql_lit, tuple(params_lit)) literal_products = await self.db.execute_query(sql_lit, tuple(params_lit))
...@@ -292,7 +291,7 @@ class SearchEngine: ...@@ -292,7 +291,7 @@ class SearchEngine:
code = p.get("internal_ref_code") code = p.get("internal_ref_code")
if code and code not in seen: if code and code not in seen:
seen.add(code); merged.append(p) seen.add(code); merged.append(p)
if merged: return merged[:10], 1, "Success (Dual-Lane)" if merged: return merged[:10], 1, "Success (Dual-Lane)"
return await self._fallback_cascade(inf) return await self._fallback_cascade(inf)
...@@ -361,25 +360,50 @@ class SearchEngine: ...@@ -361,25 +360,50 @@ class SearchEngine:
p["outfit_recommendations"] = [] p["outfit_recommendations"] = []
return products return products
async def search(self, literal: str, inferred: Dict[str, Any], check_stock: bool = True) -> Dict[str, Any]: @staticmethod
def _parse_description_data_full(products: list) -> list:
"""Parse description_text_full JSON string into structured metadata."""
for p in products:
raw = p.get("description_text_full")
p["styling_metadata"] = {}
if raw and isinstance(raw, str):
try:
data = json.loads(raw)
metadata = {}
if "phong_cach" in data: metadata["phong_cach"] = data["phong_cach"]
if "dip_mac" in data: metadata["dip_mac"] = data["dip_mac"]
if "cross_sell" in data: metadata["cross_sell"] = data["cross_sell"]
if "tinh_nang_vai" in data: metadata["tinh_nang_vai"] = data["tinh_nang_vai"]
if "tags" in data: metadata["tags"] = data["tags"]
if "mo_ta_chinh" in data and data["mo_ta_chinh"]:
p["description_text"] = data["mo_ta_chinh"]
p["styling_metadata"] = metadata
except (json.JSONDecodeError, TypeError):
pass
p.pop("description_text_full", None)
return products
async def search(self, literal: str, inferred: Dict[str, Any]) -> Dict[str, Any]:
inf = InferredSearch(**inferred) inf = InferredSearch(**inferred)
_apply_event_fallback(inf, literal) _apply_event_fallback(inf, literal)
# Hard Pattern # Hard Pattern
pattern = _hard_pattern_detector.detect(literal) pattern = _hard_pattern_detector.detect(literal)
if pattern.pattern_type == "cheaper_than" and pattern.target_price_max: if pattern.pattern_type == "cheaper_than" and pattern.target_price_max:
inf.price_max = int(pattern.target_price_max) inf.price_max = int(pattern.target_price_max)
# Underwear Override # Underwear Override
is_underwear = any(line in ["Quần lót", "Áo lót", "Sịp", "Bra"] for line in inf.product_line_vn) is_underwear = any(line in ["Quần lót", "Áo lót", "Sịp", "Bra"] for line in inf.product_line_vn)
if is_underwear: inf.gender_by_product = inf.age_by_product = None if is_underwear: inf.gender_by_product = inf.age_by_product = None
products, tier, message = await self._dual_lane_search(literal, inf) products, tier, message = await self._dual_lane_search(literal, inf)
if products: if products:
if check_stock: products = await enrich_with_stock(products)
products = self._parse_similar_items(products) products = self._parse_similar_items(products)
products = self._parse_outfit_recommendations(products) products = self._parse_outfit_recommendations(products)
products = self._parse_description_data_full(products)
for p in products: for p in products:
raw_size = p.get("size_scale", "") raw_size = p.get("size_scale", "")
parsed = [s.strip() for s in str(raw_size).replace("[", "").replace("]", "").replace('"', '').split(",") if s.strip()] parsed = [s.strip() for s in str(raw_size).replace("[", "").replace("]", "").replace('"', '').split(",") if s.strip()]
......
import sqlite3
DB_PATH = 'D:/cnf/chatbot_canifa/preference/common/database/canifa_ai_dump.sqlite'
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = [row[0] for row in cursor.fetchall()]
print('Tables:', tables)
cursor.execute("PRAGMA table_info(sr__test_db__magento_product_dimension_with_text_embedding);")
columns = [row[1] for row in cursor.fetchall()]
print('Columns:', columns)
...@@ -15,7 +15,7 @@ import os ...@@ -15,7 +15,7 @@ import os
_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PROJECT_ROOT = os.path.dirname(_BACKEND_DIR) # D:\cnf\chatbot_canifa _PROJECT_ROOT = os.path.dirname(_BACKEND_DIR) # D:\cnf\chatbot_canifa
SQLITE_DB_PATH: str = os.path.join(_PROJECT_ROOT, "preference", "common", "database", "canifa_ai_dump.sqlite") SQLITE_DB_PATH: str = os.path.join(_BACKEND_DIR, "database", "canifa_ai_dump.sqlite")
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# SQLITE TABLE NAMES (mapping schema.table → SQLite table name) # SQLITE TABLE NAMES (mapping schema.table → SQLite table name)
......
...@@ -58,6 +58,7 @@ __all__ = [ ...@@ -58,6 +58,7 @@ __all__ = [
"STARROCKS_USER", "STARROCKS_USER",
"STOCK_API_URL", "STOCK_API_URL",
"USE_MONGO_CONVERSATION", "USE_MONGO_CONVERSATION",
"USE_LANGFUSE_PROMPTS",
] ]
# ====================== SUPABASE CONFIGURATION ====================== # ====================== SUPABASE CONFIGURATION ======================
...@@ -88,10 +89,11 @@ JWT_ALGORITHM: str | None = os.getenv("JWT_ALGORITHM") ...@@ -88,10 +89,11 @@ JWT_ALGORITHM: str | None = os.getenv("JWT_ALGORITHM")
PORT: int = int(os.getenv("PORT", "5000")) PORT: int = int(os.getenv("PORT", "5000"))
FIRECRAWL_API_KEY: str | None = os.getenv("FIRECRAWL_API_KEY") FIRECRAWL_API_KEY: str | None = os.getenv("FIRECRAWL_API_KEY")
# ====================== LANGFUSE CONFIGURATION (DEPRECATED) ====================== # ====================== LANGFUSE CONFIGURATION ======================
LANGFUSE_SECRET_KEY: str | None = os.getenv("LANGFUSE_SECRET_KEY") LANGFUSE_SECRET_KEY: str | None = os.getenv("LANGFUSE_SECRET_KEY")
LANGFUSE_PUBLIC_KEY: str | None = os.getenv("LANGFUSE_PUBLIC_KEY") LANGFUSE_PUBLIC_KEY: str | None = os.getenv("LANGFUSE_PUBLIC_KEY")
LANGFUSE_BASE_URL: str | None = os.getenv("LANGFUSE_BASE_URL", "https://cloud.langfuse.com") LANGFUSE_BASE_URL: str | None = os.getenv("LANGFUSE_BASE_URL", "https://cloud.langfuse.com")
USE_LANGFUSE_PROMPTS: bool = os.getenv("USE_LANGFUSE_PROMPTS", "true").lower() == "true"
# ====================== LANGSMITH CONFIGURATION (TẮT VÌ RATE LIMIT) ====================== # ====================== LANGSMITH CONFIGURATION (TẮT VÌ RATE LIMIT) ======================
# LANGSMITH_TRACING = os.getenv("LANGSMITH_TRACING", "false") # LANGSMITH_TRACING = os.getenv("LANGSMITH_TRACING", "false")
......
# Size Column Migration
## Mục đích
Thêm cột `size` vào bảng `pg__dashboard_canifa__ultra_descriptions` để lưu **bảng size đã filter** (chỉ size thực tế có bán), giúp giảm token khi hiển thị trong chatbot.
## Files
- `migrations/002_add_size_column.sql` - Thêm cột `size`
- `populate_size_column.py` - Điền dữ liệu vào cột `size`
## Cách chạy
### Bước 1: Apply migration (thêm cột)
```bash
sqlite3 backend/database/canifa_ai_dump.sqlite < backend/database/migrations/002_add_size_column.sql
```
Hoặc mở DB bằng DB Browser for SQLite và chạy SQL:
```sql
ALTER TABLE pg__dashboard_canifa__ultra_descriptions ADD COLUMN size TEXT;
```
### Bước 2: Populate data
```bash
cd backend/database
python populate_size_column.py
```
Script sẽ:
1. Đọc `description_data->'huong_dan_size'` (bảng size đầy đủ với XS, S, M, L, XL)
2. Lọc chỉ giữ lại các size có trong `size_scale` (VD: "L, M, S, XL")
3. Ghi kết quả vào cột `size`
### Bước 3: Verify
```bash
sqlite3 backend/database/canifa_ai_dump.sqlite "SELECT id, size_scale, size FROM pg__dashboard_canifa__ultra_descriptions WHERE size IS NOT NULL LIMIT 1;"
```
Expected output:
- `size_scale`: "L, M, S, XL"
- `size`: Bảng markdown chỉ chứa S, M, L, XL (không có XS)
## Lợi ích
- Trước: `huong_dan_size` ~500 tokens (bảng đầy đủ 5-6 sizes)
- Sau: `size` ~100 tokens (chỉ sizes thực tế)
- Tiết kiệm ~400 tokens/sp × số sản phẩm hiển thị
## Notes
- Cột `size` được thêm vào SELECT trong `product_search_engine.py`
- Output `item["size_table"]` sẽ dùng cột `size` thay vì parse `huong_dan_size` mỗi lần
"""Quick check: list all tables in both SQLite DBs and show outfit-related data."""
import os
import sqlite3
def check_db(db_path, label):
if not os.path.exists(db_path):
print(f"\n❌ {label}: File not found at {db_path}")
return
size_mb = os.path.getsize(db_path) / (1024 * 1024)
print(f"\n{'='*60}")
print(f"📦 {label}")
print(f" Path: {db_path}")
print(f" Size: {size_mb:.1f} MB")
print(f"{'='*60}")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
# List all tables
cur.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = [r[0] for r in cur.fetchall()]
print(f"\n📋 Tables ({len(tables)}):")
for t in tables:
cur.execute(f'SELECT COUNT(*) FROM "{t}"')
cnt = cur.fetchone()[0]
marker = " ⭐" if "outfit" in t.lower() else ""
print(f" • {t:50s} ({cnt:,} rows){marker}")
# Check outfit-specific tables
outfit_tables = [t for t in tables if "outfit" in t.lower()]
if outfit_tables:
print(f"\n🎨 Outfit Tables Detail:")
for t in outfit_tables:
print(f"\n >>> {t}")
cur.execute(f'PRAGMA table_info("{t}")')
cols = cur.fetchall()
print(f" Columns:")
for c in cols:
print(f" {c[1]:20s} {c[2]:15s} {'PK' if c[5] else ''}")
# Show sample data
cur.execute(f'SELECT * FROM "{t}" LIMIT 5')
sample = cur.fetchall()
if sample:
col_names = [c[1] for c in cols]
print(f" Sample data ({min(5, len(sample))} rows):")
for row in sample:
print(f" {dict(zip(col_names, row))}")
else:
print(f" (empty table)")
conn.close()
if __name__ == "__main__":
base = os.path.dirname(os.path.abspath(__file__))
backend = os.path.dirname(base)
# Check 123.db
check_db(os.path.join(backend, "123.db"), "123.db (Migration DB)")
# Check canifa_ai_dump.sqlite
check_db(os.path.join(base, "canifa_ai_dump.sqlite"), "canifa_ai_dump.sqlite (Main App DB)")
import os
import sqlite3
db_path = r"C:\canifa-idea\chatbot-canifa-feedback\backend\database\canifa_ai_dump.sqlite"
if not os.path.exists(db_path):
print("Database not found!")
else:
db = sqlite3.connect(db_path)
c = db.cursor()
c.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = c.fetchall()
print("Tables in Sqlite:", tables)
import sqlite3
def inspect():
db_path = 'canifa_ai_dump.sqlite'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='sr__test_db__magento_product_dimension_with_text_embedding';")
schema = cursor.fetchone()
print("Schema of products:")
print(schema[0])
conn.close()
if __name__ == '__main__':
inspect()
import os
import sqlite3
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from common.constants import SQLITE_DB_PATH
DB_PATH = SQLITE_DB_PATH
def migrate():
print(f"Connecting to database at {DB_PATH}...")
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create the table
create_table_sql = """
CREATE TABLE IF NOT EXISTS pg__dashboard_canifa__ai_outfit_product_matches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id INTEGER NOT NULL,
anchor_product_code VARCHAR(255) NOT NULL,
match_product_code VARCHAR(255) NOT NULL,
match_product_name VARCHAR(255),
match_gender VARCHAR(50),
match_age VARCHAR(50),
match_role VARCHAR(50) NOT NULL,
score FLOAT NOT NULL,
ai_reason TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
print("Executing DROP and CREATE TABLE statement for pg__dashboard_canifa__ai_outfit_product_matches...")
cursor.execute("DROP TABLE IF EXISTS pg__dashboard_canifa__ai_outfit_product_matches;")
cursor.execute(create_table_sql)
# Create indexes for fast lookup
print("Creating indexes for fast lookup...")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_anchor_product ON pg__dashboard_canifa__ai_outfit_product_matches(anchor_product_code);")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_rule_id ON pg__dashboard_canifa__ai_outfit_product_matches(rule_id);")
conn.commit()
print("Migration successful! Table pg__dashboard_canifa__ai_outfit_product_matches created.")
conn.close()
if __name__ == "__main__":
migrate()
import requests
links_to_restore = [
{
"id": "916c2a97",
"title": "Langfuse Monitoring",
"url": "http://172.16.2.207:3009",
"description": "LLM observability & tracing dashboard — theo dõi traces, scores, prompts",
"category": "tool",
"icon": "🔍",
"pinned": True
},
{
"id": "a1b2c301",
"title": "LiteLLM Proxy",
"url": "http://172.16.2.207:4000",
"description": "LLM Gateway — proxy OpenAI/Gemini/Ollama models, quản lý API keys & rate limits",
"category": "tool",
"icon": "🤖",
"pinned": True
},
{
"id": "a1b2c302",
"title": "n8n Workflows",
"url": "http://172.16.2.207:5678",
"description": "Workflow automation — test chatbot, auto-test hàng ngày, tích hợp Google Sheets",
"category": "tool",
"icon": "⚡",
"pinned": False
},
{
"id": "a1b2c306",
"title": "n8n Auto-Test Workflow",
"url": "http://172.16.2.207:5678/workflow/EPB44XlnVAi6FCnl",
"description": "Workflow test tự động chatbot — chạy daily 8:00 AM, log kết quả lên Google Sheets",
"category": "tool",
"icon": "🧪",
"pinned": False
},
{
"id": "a1b2c307",
"title": "Test Conversation Observer",
"url": "http://172.16.2.207:5000/static/history.html",
"description": "Xem lịch sử hội thoại test — review chất lượng bot responses",
"category": "tool",
"icon": "🧾",
"pinned": False
},
{
"id": "a1b2c308",
"title": "Redis Cache",
"url": "redis://172.16.2.207:6379",
"description": "Cache layer — response cache, user insight, session data",
"category": "api",
"icon": "🗄️",
"pinned": False
},
{
"id": "a1b2c309",
"title": "Chatbot API Docs",
"url": "http://172.16.2.207:5000/docs",
"description": "Swagger UI — tất cả API endpoints, test trực tiếp trên browser",
"category": "api",
"icon": "📚",
"pinned": False
},
{
"id": "a1b2c310",
"title": "ReDoc API Reference",
"url": "http://172.16.2.207:5000/redoc",
"description": "API documentation dạng ReDoc — clean, dễ đọc",
"category": "doc",
"icon": "📖",
"pinned": False
},
{
"id": "2912d187",
"title": "tool test conservation",
"url": "http://172.16.2.207:5004/static/test_conversation.html",
"description": "Tools test conservations, user có thể nhập url (trở tới bản dev hoặc stage) ném vào 1 file excel đã chuẩn bị sẵn",
"category": "tool",
"icon": "🔧",
"pinned": False
}
]
url = "http://localhost:5000/api/dashboard/links"
for link in links_to_restore:
# use their original ID by issuing a POST but actually the backend POST ignores ID and generates a new one.
# We will just insert them fresh!
res = requests.post(url, json=link)
print(res.status_code)
import sqlite3
rules_data = [
# WOMEN - di_lam
('women', 'adult', 'di_lam', 'all', 'Áo kiểu', 'bottom', 'Chân váy', 'Neutral+Neutral', 'Áo kiểu phối chân váy tạo vẻ nữ tính, thanh lịch cho môi trường công sở.'),
('women', 'adult', 'di_lam', 'all', 'Áo kiểu', 'bottom', 'Quần dài', 'Neutral+Neutral', 'Áo kiểu phối quần dài mang lại sự chỉn chu và thoải mái cả ngày.'),
('women', 'adult', 'di_lam', 'all', 'Blouse', 'bottom', 'Chân váy', 'Neutral+Light', 'Áo blouse nhẹ nhàng kết hợp chân váy tạo phong cách công sở trẻ trung, nữ tính.'),
('women', 'adult', 'di_lam', 'all', 'Blouse', 'bottom', 'Quần dài', 'Neutral+Neutral', 'Blouse mềm mại phối cùng quần dài tạo vẻ ngoài chuyên nghiệp và hiện đại.'),
('women', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'bottom', 'Quần Khaki', 'Neutral+Neutral', 'Sơ mi thanh lịch đi cùng quần khaki là bộ đôi hoàn hảo, lịch sự cho ngày làm việc.'),
('women', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'bottom', 'Chân váy', 'Neutral+Light', 'Sơ mi phối chân váy giúp tôn dáng, mang lại vẻ ngoài tinh tế nơi công sở.'),
('women', 'adult', 'di_lam', 'all', 'Áo Polo', 'bottom', 'Quần Khaki', 'Neutral+Neutral', 'Polo năng động nhưng vẫn đủ chỉn chu khi kết hợp với quần khaki đi làm.'),
('women', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'outer', 'Blazer', 'Neutral+Neutral', 'Thêm áo khoác Blazer bên ngoài sơ mi để tăng gấp đôi sự chuyên nghiệp và quyền lực.'),
('women', 'adult', 'di_lam', 'all', 'Blouse', 'outer', 'Blazer', 'Neutral+Neutral', 'Blazer khoác ngoài áo blouse tạo điểm nhấn trang nhã, ấm áp cho ngày se lạnh.'),
('women', 'adult', 'di_lam', 'all', 'Áo kiểu', 'outer', 'Cardigan', 'Neutral+Light', 'Khoác thêm Cardigan bên ngoài áo kiểu mang đến sự mềm mại, ấm áp mà vẫn lịch sự.'),
# WOMEN - di_choi
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần jean', 'Neutral+Dark', 'Combo áo phông và quần jean là lựa chọn kinh điển, năng động tuyệt đối để đi chơi.'),
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần soóc', 'Neutral+Dark', 'Áo phông phối quần soóc thoải mái, lý tưởng cho những buổi dạo phố cuối tuần.'),
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'bottom', 'Chân váy', 'Light+Neutral', 'Trẻ trung, năng động nhưng vẫn điệu đà khi mix áo phông cùng chân váy.'),
('women', 'adult', 'di_choi', 'all', 'Áo kiểu', 'bottom', 'Quần jean', 'Light+Neutral', 'Áo kiểu nữ tính kết hợp quần jean mang lại vẻ ngoài vừa điệu đà vừa cá tính.'),
('women', 'adult', 'di_choi', 'all', 'Áo kiểu', 'bottom', 'Chân váy', 'Light+Neutral', 'Set đồ áo kiểu và chân váy hoàn hảo cho những buổi hẹn hò, cà phê nữ tính.'),
('women', 'adult', 'di_choi', 'all', 'Áo nỉ', 'bottom', 'Quần jean', 'Neutral+Dark', 'Áo nỉ ấm áp phối cùng quần jean cực kỳ hợp mốt và năng động cho ngày dạo phố.'),
('women', 'adult', 'di_choi', 'summer', 'Áo hai dây', 'bottom', 'Quần jean', 'Light+Neutral', 'Áo hai dây mát mẻ mix cùng quần jean tạo phong cách quyến rũ, hiện đại.'),
('women', 'adult', 'di_choi', 'summer', 'Áo hai dây', 'bottom', 'Chân váy', 'Light+Neutral', 'Gợi cảm và điệu đà với áo hai dây phối cùng chân váy dạo phố.'),
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'outer', 'Áo khoác gió', 'Neutral+Neutral', 'Áo khoác gió khoác ngoài áo phông giúp cản gió tốt mà vẫn giữ được độ khỏe khoắn.'),
('women', 'adult', 'di_choi', 'all', 'Áo hai dây', 'outer', 'Cardigan', 'Light+Neutral', 'Thêm một chiếc Cardigan mỏng bên ngoài áo hai dây giúp che chắn nhẹ nhàng, thanh lịch.'),
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'accessory', 'Túi xách', 'Neutral+Neutral', 'Tô điểm thêm một chiếc túi xách để hoàn thiện outfit dạo phố.'),
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'accessory', 'Mũ', 'Neutral+Neutral', 'Một chiếc mũ không chỉ che nắng mà còn tăng thêm độ cool ngầu cho set đồ.'),
# WOMEN - mac_nha
('women', 'adult', 'mac_nha', 'summer', 'Áo hai dây', 'bottom', 'Quần mặc nhà', 'Light+Neutral', 'Áo hai dây phối quần mặc nhà mỏng nhẹ, mang lại sự thoải mái tuyệt đối.'),
('women', 'adult', 'mac_nha', 'summer', 'Áo hai dây', 'bottom', 'Quần soóc', 'Light+Neutral', 'Combo áo hai dây và quần soóc siêu mát mẻ cho những ngày hè oi bức ở nhà.'),
('women', 'adult', 'mac_nha', 'all', 'Áo phông', 'bottom', 'Quần mặc nhà', 'Neutral+Neutral', 'Áo phông cotton thấm hút tốt phối quần mặc nhà rộng rãi, dễ chịu.'),
('women', 'adult', 'mac_nha', 'all', 'Bộ mặc nhà', 'set', 'Bộ mặc nhà', 'Neutral+Light', 'Bộ mặc nhà tông sáng mang lại cảm giác thư giãn, nhẹ nhàng.'),
('women', 'adult', 'mac_nha', 'all', 'Pyjama', 'set', 'Pyjama', 'Light+Light', 'Pyjama mềm mại giúp bạn có giấc ngủ ngon và vẻ ngoài ưng ý ngay cả khi ở nhà.'),
# WOMEN - du_lich_bien
('women', 'adult', 'du_lich_bien', 'summer', 'Áo hai dây', 'bottom', 'Chân váy', 'Light+Neutral', 'Áo hai dây phối chân váy bồng bềnh cực kỳ hợp cảnh biển xanh nắng vàng.'),
('women', 'adult', 'du_lich_bien', 'summer', 'Áo hai dây', 'bottom', 'Quần soóc', 'Light+Dark', 'Năng động, phóng khoáng với áo hai dây và quần soóc dạo biển.'),
('women', 'adult', 'du_lich_bien', 'summer', 'Váy liền', 'set', 'Váy liền', 'Light+Light', 'Một chiếc váy liền họa tiết rực rỡ là must-have item để sống ảo trên bãi biển.'),
('women', 'adult', 'du_lich_bien', 'summer', 'Áo phông', 'accessory', 'Mũ', 'Neutral+Neutral', 'Đừng quên một chiếc mũ rộng vành để che nắng và tạo dáng chụp ảnh.'),
# WOMEN - du_lich_nui
('women', 'adult', 'du_lich_nui', 'all', 'Áo nỉ có mũ', 'bottom', 'Quần leggings', 'Dark+Neutral', 'Áo nỉ có mũ giữ ấm tốt, phối cùng leggings co giãn dễ dàng leo núi, vận động.'),
('women', 'adult', 'du_lich_nui', 'all', 'Áo phông', 'bottom', 'Quần leggings', 'Neutral+Dark', 'Áo phông thoáng mát và leggings ôm gọn là combo hoàn hảo để đi bộ khám phá.'),
('women', 'adult', 'du_lich_nui', 'all', 'Áo khoác gió', 'outer', 'Quần leggings', 'Dark+Neutral', 'Áo khoác gió siêu nhẹ chắn gió sương vùng núi, bắt cặp hoàn hảo với leggings.'),
('women', 'adult', 'du_lich_nui', 'all', 'Áo phông', 'accessory', 'Túi xách', 'Neutral+Neutral', 'Sử dụng balo hoặc túi đeo chéo gọn nhẹ để đựng đồ cá nhân khi di chuyển.'),
# WOMEN - summer specific
('women', 'adult', 'di_choi', 'summer', 'Áo kiểu', 'bottom', 'Chân váy', 'Light+Neutral', 'Outfit mùa hè tươi tắn, dịu dàng với áo kiểu mỏng nhẹ và chân váy lụa/voan.'),
('women', 'adult', 'di_choi', 'summer', 'Áo hai dây', 'bottom', 'Quần culottes', 'Light+Neutral', 'Thoáng mát, rộng rãi mà vẫn sành điệu với áo hai dây và quần ống rộng culottes.'),
('women', 'adult', 'di_choi', 'summer', 'Áo hai dây', 'outer', 'Áo khoác chống nắng', 'Neutral+Neutral', 'Ra đường ngày hè đừng quên khoác thêm áo chống nắng để bảo vệ làn da.'),
('women', 'adult', 'di_choi', 'summer', 'Áo kiểu', 'accessory', 'Túi xách', 'Neutral+Neutral', 'Một chiếc túi cói hoặc túi canvas sẽ rất hợp vibe mùa hè.'),
# WOMEN - winter specific
('women', 'adult', 'di_choi', 'winter', 'Áo Body', 'bottom', 'Quần leggings', 'Neutral+Neutral', 'Giữ ấm tuyệt đối nhưng vẫn tôn dáng với áo body ôm sát và leggings lót nỉ.'),
('women', 'adult', 'di_choi', 'winter', 'Áo Body', 'outer', 'Cardigan', 'Neutral+Neutral', 'Khoác thêm Cardigan dày dặn để tăng độ ấm và tạo layer phong cách.'),
('women', 'adult', 'di_lam', 'winter', 'Áo Body', 'outer', 'Blazer', 'Neutral+Dark', 'Mùa đông vẫn thanh lịch công sở khi phối áo body giữ nhiệt bên trong Blazer.'),
('women', 'adult', 'di_choi', 'winter', 'Cardigan', 'outer', 'Áo khoác dạ', 'Neutral+Neutral', 'Áo khoác dạ khoác ngoài Cardigan tạo nên phong cách layering ấm áp, sang trọng.'),
('women', 'adult', 'di_choi', 'winter', 'Áo len', 'bottom', 'Quần leggings', 'Neutral+Neutral', 'Áo len oversize mix cùng leggings là công thức chuẩn mùa đông, vừa ấm vừa đáng yêu.'),
('women', 'adult', 'di_choi', 'winter', 'Áo len', 'accessory', 'Khăn', 'Neutral+Neutral', 'Thêm một chiếc khăn quàng cổ tông xuyệt tông để hoàn thiện set đồ ấm áp.'),
# WOMEN - all (xuan thu)
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần váy', 'Neutral+Light', 'Quần váy giả váy giúp bạn tự tin vận động mà vẫn giữ được nét điệu đà, hợp phối cùng áo phông.'),
('women', 'adult', 'di_choi', 'all', 'Áo Polo', 'bottom', 'Quần váy', 'Neutral+Neutral', 'Polo có cổ phối cùng quần váy tạo vẻ ngoài năng động, khỏe khoắn phong cách thể thao.'),
('women', 'adult', 'di_choi', 'all', 'Váy liền', 'set', 'Váy liền', 'Neutral+Light', 'Tiết kiệm thời gian phối đồ với một chiếc váy liền thanh lịch, phù hợp thời tiết mát mẻ.'),
('women', 'adult', 'di_choi', 'all', 'Áo phông', 'outer', 'Áo khoác gió', 'Neutral+Neutral', 'Thời tiết giao mùa chỉ cần khoác nhẹ áo khoác gió bên ngoài áo phông là đủ.'),
# MEN - di_lam
('men', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'bottom', 'Quần Khaki', 'Neutral+Neutral', 'Áo sơ mi kết hợp quần khaki là sự chuẩn mực, tạo vẻ ngoài chuyên nghiệp và chững chạc.'),
('men', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'bottom', 'Quần dài', 'Neutral+Neutral', 'Sơ mi và quần tây dài là trang phục công sở kinh điển cho phái mạnh.'),
('men', 'adult', 'di_lam', 'all', 'Áo Polo', 'bottom', 'Quần Khaki', 'Neutral+Neutral', 'Với những ngày thứ Sáu thoải mái, Polo phối quần khaki mang lại vẻ năng động mà vẫn lịch sự.'),
('men', 'adult', 'di_lam', 'all', 'Áo Polo', 'bottom', 'Quần dài', 'Neutral+Neutral', 'Polo sơ vin cùng quần âu dài cũng rất phù hợp cho môi trường văn phòng trẻ trung.'),
('men', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'outer', 'Blazer', 'Neutral+Neutral', 'Tăng thêm phần sang trọng, quyền lực cho buổi họp quan trọng với áo khoác Blazer.'),
# MEN - di_choi
('men', 'adult', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần jean', 'Neutral+Dark', 'Đơn giản, khỏe khoắn với combo áo phông và quần jean cá tính.'),
('men', 'adult', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần soóc', 'Neutral+Dark', 'Thoải mái tối đa dạo phố cuối tuần với áo phông và quần soóc năng động.'),
('men', 'adult', 'di_choi', 'all', 'Áo nỉ', 'bottom', 'Quần jean', 'Dark+Neutral', 'Áo nỉ thể thao mix quần jean mang lại vẻ ngoài bụi bặm, mạnh mẽ.'),
('men', 'adult', 'di_choi', 'all', 'Áo phông', 'accessory', 'Mũ', 'Neutral+Neutral', 'Kết hợp thêm một chiếc mũ lưỡi trai để set đồ dạo phố thêm phần cá tính.'),
# MEN - mac_nha
('men', 'adult', 'mac_nha', 'all', 'Áo ba lỗ', 'bottom', 'Quần soóc', 'Neutral+Neutral', 'Áo ba lỗ mát mẻ phối quần soóc rộng rãi, cực kỳ lý tưởng để thư giãn tại nhà.'),
('men', 'adult', 'mac_nha', 'all', 'Áo phông', 'bottom', 'Quần soóc', 'Neutral+Neutral', 'Set áo phông cotton và quần soóc thoải mái mặc nhà hay chạy bộ nhẹ nhàng.'),
('men', 'adult', 'mac_nha', 'all', 'Bộ mặc nhà', 'set', 'Bộ mặc nhà', 'Neutral+Neutral', 'Bộ mặc nhà đồng bộ mang lại cảm giác thoải mái và gọn gàng.'),
# MEN - du_lich_bien
('men', 'adult', 'du_lich_bien', 'summer', 'Áo Sơ mi', 'bottom', 'Quần soóc', 'Light+Neutral', 'Sơ mi ngắn tay mở cúc nhẹ phối quần soóc lanh là chuẩn phong cách đi biển.'),
('men', 'adult', 'du_lich_bien', 'summer', 'Áo ba lỗ', 'bottom', 'Quần soóc', 'Neutral+Neutral', 'Mát mẻ, phóng khoáng với áo ba lỗ và quần soóc dạo bờ biển.'),
# MEN - du_lich_nui
('men', 'adult', 'du_lich_nui', 'all', 'Áo khoác gió', 'bottom', 'Quần dài', 'Dark+Neutral', 'Áo khoác gió nhẹ cản gió tốt kết hợp quần dài thể thao dễ dàng leo núi.'),
('men', 'adult', 'du_lich_nui', 'all', 'Áo Polo', 'bottom', 'Quần dài', 'Neutral+Neutral', 'Áo Polo co giãn thấm hút phối quần túi hộp dài tạo phong cách dã ngoại mạnh mẽ.'),
('men', 'adult', 'du_lich_nui', 'all', 'Áo Polo', 'accessory', 'Mũ', 'Neutral+Neutral', 'Mũ bucket hoặc mũ lưỡi trai là phụ kiện không thể thiếu khi khám phá thiên nhiên.'),
# MEN - summer
('men', 'adult', 'di_choi', 'summer', 'Áo Polo', 'bottom', 'Quần soóc', 'Neutral+Neutral', 'Polo lịch sự kết hợp quần soóc mát mẻ cho những ngày hè đi dạo hay cà phê.'),
('men', 'adult', 'di_choi', 'summer', 'Áo phông', 'bottom', 'Quần Khaki', 'Neutral+Neutral', 'Áo phông trơn phối quần khaki sáng màu mang lại vẻ ngoài trẻ trung, sáng sủa.'),
('men', 'adult', 'di_choi', 'summer', 'Áo Polo', 'accessory', 'Mũ', 'Neutral+Neutral', 'Mũ lưỡi trai vừa che nắng vừa tăng độ năng động cho set đồ.'),
# MEN - winter
('men', 'adult', 'di_choi', 'winter', 'Áo giữ nhiệt', 'outer', 'Áo nỉ', 'Neutral+Neutral', 'Lót áo giữ nhiệt bên trong áo nỉ giúp giữ ấm cơ thể mà không bị cộm.'),
('men', 'adult', 'di_choi', 'winter', 'Áo nỉ', 'outer', 'Áo khoác lông vũ', 'Neutral+Dark', 'Khoác thêm áo lông vũ siêu nhẹ bên ngoài để chống chịu những ngày đại hàn.'),
('men', 'adult', 'di_choi', 'winter', 'Áo giữ nhiệt', 'bottom', 'Quần jean', 'Neutral+Dark', 'Áo giữ nhiệt mặc lót trong phối quần jean dày dặn là combo chống rét cơ bản.'),
('men', 'adult', 'di_choi', 'winter', 'Áo nỉ', 'bottom', 'Quần nỉ', 'Neutral+Neutral', 'Set áo nỉ và quần nỉ thể thao đồng bộ, ấm áp và cực kỳ thoải mái vận động.'),
('men', 'adult', 'di_choi', 'winter', 'Áo len', 'bottom', 'Quần jean', 'Neutral+Neutral', 'Áo len vặn thừng kết hợp quần jean mang lại vẻ nam tính, ấm áp cổ điển.'),
('men', 'adult', 'di_choi', 'winter', 'Áo len', 'accessory', 'Tất', 'Neutral+Neutral', 'Đừng quên những đôi tất len cao cổ để giữ ấm và tạo điểm nhấn mix&match.'),
# MEN - xuan thu
('men', 'adult', 'di_choi', 'all', 'Áo Sơ mi', 'bottom', 'Quần jean', 'Neutral+Neutral', 'Sơ mi khoác ngoài áo thun mỏng phối quần jean bụi bặm cực hợp tiết trời thu.'),
('men', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'bottom', 'Quần Khaki', 'Neutral+Neutral', 'Chỉn chu, lịch thiệp nơi công sở với sơ mi dài tay và quần khaki phom chuẩn.'),
# GIRL (kid)
('girl', 'kid', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần dài', 'Light+Neutral', 'Áo phông in hình dễ thương phối quần dài thun co giãn cho bé thoải mái vui chơi.'),
('girl', 'kid', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần soóc', 'Light+Neutral', 'Mát mẻ và đáng yêu khi bé mặc áo phông mix cùng quần soóc dạo phố.'),
('girl', 'kid', 'di_choi', 'all', 'Áo phông', 'accessory', 'Mũ', 'Light+Light', 'Thêm một chiếc mũ rộng vành đáng yêu để che nắng cho bé.'),
('girl', 'kid', 'di_choi', 'all', 'Áo phông', 'accessory', 'Khăn', 'Light+Light', 'Một chiếc khăn họa tiết nhỏ thắt cổ tạo điểm nhấn tinh nghịch.'),
('girl', 'kid', 'mac_nha', 'summer', 'Áo hai dây', 'bottom', 'Quần soóc', 'Light+Neutral', 'Áo hai dây và quần đùi vải lanh siêu thoáng mát cho bé ở nhà ngày nóng.'),
('girl', 'kid', 'mac_nha', 'all', 'Bộ mặc nhà', 'set', 'Bộ mặc nhà', 'Light+Light', 'Bộ đồ mặc nhà cotton thấm hút mồ hôi, mềm mịn với da bé.'),
('girl', 'kid', 'du_lich', 'summer', 'Bộ quần áo', 'set', 'Bộ quần áo', 'Light+Neutral', 'Set bộ đi chơi đồng bộ họa tiết nhí nhảnh giúp mẹ đỡ đau đầu phối đồ cho bé.'),
('girl', 'kid', 'du_lich', 'all', 'Áo phông', 'bottom', 'Quần dài', 'Light+Neutral', 'Combo năng động chống muỗi, chống nắng tốt cho bé khi tham gia dã ngoại.'),
('girl', 'kid', 'du_lich', 'all', 'Áo phông', 'accessory', 'Mũ', 'Light+Light', 'Trang bị ngay một chiếc mũ tai bèo che nắng bảo vệ bé yêu.'),
('girl', 'kid', 'summer', 'summer', 'Bộ quần áo', 'set', 'Bộ quần áo', 'Light+Light', 'Những set bộ cotton, lanh mỏng nhẹ là ưu tiên số một cho bé vào mùa hè.'),
('girl', 'kid', 'winter', 'winter', 'Áo khoác gilet chần bông', 'bottom', 'Quần nỉ', 'Neutral+Neutral', 'Áo gilet giữ ấm phần ngực bé mà vẫn không cản trở vận động của hai tay.'),
('girl', 'kid', 'winter', 'winter', 'Áo len', 'bottom', 'Quần giữ nhiệt', 'Light+Neutral', 'Áo len chui đầu ấm áp phối legging giữ nhiệt giúp bé tha hồ chạy nhảy mùa đông.'),
('girl', 'kid', 'winter', 'winter', 'Áo len', 'accessory', 'Găng tay chống nắng', 'Neutral+Neutral', 'Găng tay giữ ấm những ngón tay nhỏ xíu khi trời rét.'),
('girl', 'kid', 'winter', 'winter', 'Áo len', 'accessory', 'Khăn', 'Light+Neutral', 'Khăn quàng len là trợ thủ đắc lực giữ ấm cổ bé.'),
('girl', 'kid', 'di_choi', 'all', 'Áo phông', 'bottom', 'Chân váy', 'Light+Neutral', 'Điệu đà như một cô công chúa nhỏ với áo phông xinh xắn và chân váy xòe.'),
('girl', 'kid', 'di_choi', 'all', 'Áo Sơ mi', 'bottom', 'Quần jean', 'Light+Neutral', 'Sơ mi kẻ caro khoác ngoài kết hợp quần jean mang lại vẻ khỏe khoắn, hiện đại.'),
('girl', 'kid', 'di_choi', 'all', 'Váy liền', 'set', 'Váy liền', 'Light+Neutral', 'Chỉ cần một chiếc đầm liền xinh xắn là bé đã sẵn sàng đi dự tiệc hoặc dạo phố.'),
# BOY (kid)
('boy', 'kid', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần dài', 'Dark+Neutral', 'Áo phông siêu anh hùng phối quần dài bo gấu cho bé trai thỏa sức chạy nhảy.'),
('boy', 'kid', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần soóc', 'Dark+Neutral', 'Quần soóc kaki túi hộp mix áo phông cực cool ngầu và mát mẻ.'),
('boy', 'kid', 'di_choi', 'all', 'Áo phông', 'accessory', 'Mũ', 'Dark+Dark', 'Mũ lưỡi trai cá tính bảo vệ bé dưới ánh nắng mặt trời.'),
('boy', 'kid', 'mac_nha', 'summer', 'Áo ba lỗ', 'bottom', 'Quần soóc', 'Dark+Neutral', 'Set áo ba lỗ quần đùi cotton lý tưởng cho những cậu bé hay ra mồ hôi lúc ở nhà.'),
('boy', 'kid', 'mac_nha', 'all', 'Bộ mặc nhà', 'set', 'Bộ mặc nhà', 'Dark+Neutral', 'Đồ bộ thun trơn thoải mái giúp bé ngủ ngon và vận động ở nhà.'),
('boy', 'kid', 'du_lich', 'all', 'Bộ quần áo', 'set', 'Bộ quần áo', 'Dark+Neutral', 'Set đồ thể thao đồng bộ rất phù hợp và tiện lợi cho những chuyến đi xa.'),
('boy', 'kid', 'du_lich', 'all', 'Áo phông', 'bottom', 'Quần dài', 'Dark+Neutral', 'Quần dài co giãn bảo vệ chân bé chống xước xát khi khám phá thiên nhiên.'),
('boy', 'kid', 'summer', 'summer', 'Bộ quần áo', 'set', 'Bộ quần áo', 'Dark+Neutral', 'Ưu tiên những set đồ cotton mỏng nhẹ, thấm hút mồ hôi siêu tốc trong mùa hè.'),
('boy', 'kid', 'winter', 'winter', 'Áo khoác gilet chần bông', 'bottom', 'Quần nỉ', 'Dark+Neutral', 'Áo gilet khoác ngoài giữ ấm lõi cơ thể mà bé vẫn vung vẩy tay linh hoạt được.'),
('boy', 'kid', 'winter', 'winter', 'Áo khoác gilet chần bông', 'bottom', 'Quần giữ nhiệt', 'Dark+Neutral', 'Kết hợp thêm quần lót giữ nhiệt bên trong để chống chọi cái lạnh đại hàn.'),
('boy', 'kid', 'winter', 'winter', 'Áo khoác gilet chần bông', 'accessory', 'Găng tay chống nắng', 'Dark+Neutral', 'Găng tay len giúp đôi bàn tay bé luôn ấm áp khi chơi ngoài trời.'),
('boy', 'kid', 'winter', 'winter', 'Áo khoác gilet chần bông', 'accessory', 'Khăn', 'Dark+Neutral', 'Khăn ống quàng cổ tiện lợi, không lo bị tuột khi bé chạy giỡn.'),
('boy', 'kid', 'di_choi', 'all', 'Áo phông', 'bottom', 'Quần nỉ', 'Dark+Neutral', 'Quần nỉ thể thao mix áo phông dài tay cho ngày giao mùa se lạnh.'),
('boy', 'kid', 'di_choi', 'all', 'Áo Polo', 'bottom', 'Quần leggings', 'Neutral+Neutral', 'Áo Polo lịch sự phối quần ôm năng động cho bé trai vẻ ngoài bảnh bao.'),
# UNISEX (adult)
('unisex', 'adult', 'di_lam', 'all', 'Áo Sơ mi', 'bottom', 'Quần dài', 'Neutral+Neutral', 'Sơ mi dáng suông oversize kết hợp quần tây ống rộng mang lại vẻ thanh lịch phi giới tính.'),
('unisex', 'adult', 'di_choi', 'all', 'Bộ thể thao', 'set', 'Bộ thể thao', 'Neutral+Dark', 'Set đồ thể thao đồng bộ mạnh mẽ, thoải mái và đậm chất đường phố năng động.')
]
# We will drop the old table and recreate the v2
drop_sql = "DROP TABLE IF EXISTS pg__dashboard_canifa__ai_outfit_rules;"
create_sql = """
CREATE TABLE pg__dashboard_canifa__ai_outfit_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gender TEXT,
age_group TEXT,
occasion TEXT,
season TEXT,
anchor_category TEXT,
match_role TEXT,
target_category TEXT,
color_rule TEXT,
ai_reason TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
insert_sql = """
INSERT INTO pg__dashboard_canifa__ai_outfit_rules
(gender, age_group, occasion, season, anchor_category, match_role, target_category, color_rule, ai_reason)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
conn = sqlite3.connect('123.db')
cursor = conn.cursor()
cursor.execute(drop_sql)
cursor.execute(create_sql)
cursor.executemany(insert_sql, rules_data)
conn.commit()
# Print result
cursor.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ai_outfit_rules;")
print(f"Da chen thanh cong {cursor.fetchone()[0]} fashion rules vao bang ai_outfit_rules!")
conn.close()
import json
import psycopg
def check_data():
print("Initiating DB connection...")
try:
conn = psycopg.connect("host=160.191.50.138 port=5432 dbname=law_bot user=law_user password=zvPQhfGgYwhY0ihKOTRNjN4wH")
cur = conn.cursor()
query = """
SELECT magento_ref_code, name, gender, ai_matches
FROM dashboard_canifa.ultra_descriptions
WHERE ai_matches IS NOT NULL AND ai_matches != '{}'::jsonb
AND (gender = 'Nam' OR gender = 'Nữ' OR gender = 'nam' OR gender = 'nu')
LIMIT 10;
"""
cur.execute(query)
rows = cur.fetchall()
print(f"Found {len(rows)} products with ai_matches")
for row in rows:
code = row[0]
name = row[1]
gender = row[2]
matches = row[3]
print(f"\n--- Code: {code} | Name: {name} | Gender: {gender} ---")
if isinstance(matches, str):
matches = json.loads(matches)
if not matches:
print(" No match data!")
continue
has_issue = False
for occasion, roles in matches.items():
print(f" Occasion: {occasion}")
for role, items in roles.items():
for item in items[:3]:
item_gender = item.get('gender', 'unknown').lower()
item_name = item.get('name', 'unknown')
item_cat = item.get('category', 'unknown').lower()
is_kid = ('bé' in item_gender or 'be_' in item_gender or 'girl' in item_gender or
'boy' in item_gender or 'be gai' in item_gender or 'be trai' in item_gender)
is_kid_name = ('bé' in item_name.lower())
if is_kid or is_kid_name:
has_issue = True
print(f" -> {item.get('code')}: {item_name}")
print(f" *** ISSUE: KID'S ITEM (Gender: {item_gender}) IN ADULT MATCH ({gender}) ***")
if not has_issue:
print(" OK (No demographic mismatch found in top items).")
cur.close()
conn.close()
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
check_data()
"""
migrate_001_ai_outfit_tables.py
────────────────────────────────────────────────────────────
Migration: Tạo 2 bảng ai_outfit_set + ai_outfit_items
- Tự động chọn backend dựa theo env USE_LOCAL_SQLITE:
USE_LOCAL_SQLITE=true → SQLite (local dev / home)
USE_LOCAL_SQLITE=false → Postgres (company / production)
Cách chạy:
python backend/database/migrate/migrate_001_ai_outfit_tables.py
Hoặc được gọi tự động qua outfit_db.ensure_tables() khi server khởi động.
"""
import logging
import os
import sys
logger = logging.getLogger(__name__)
SCHEMA = "dashboard_canifa"
SET_TABLE = f"{SCHEMA}.ai_outfit_set"
ITEMS_TABLE = f"{SCHEMA}.ai_outfit_items"
USE_SQLITE = os.getenv("USE_LOCAL_SQLITE", "false").strip().lower() == "true"
# ─── SQLite DDL (không có schema prefix, không có TIMESTAMPTZ) ────────────────
SQLITE_DDL = """
CREATE TABLE IF NOT EXISTS ai_outfit_set (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_code TEXT NOT NULL,
occasion_tag TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE (source_code, occasion_tag)
);
CREATE INDEX IF NOT EXISTS idx_outfit_set_source
ON ai_outfit_set(source_code);
CREATE TABLE IF NOT EXISTS ai_outfit_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
outfit_id INTEGER NOT NULL REFERENCES ai_outfit_set(id) ON DELETE CASCADE,
target_code TEXT NOT NULL,
role TEXT NOT NULL,
rank INTEGER NOT NULL DEFAULT 1,
score INTEGER DEFAULT 0,
reason TEXT DEFAULT '',
is_pinned INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_outfit_items_outfit_id
ON ai_outfit_items(outfit_id);
CREATE INDEX IF NOT EXISTS idx_outfit_items_target
ON ai_outfit_items(target_code);
"""
# ─── Postgres DDL ─────────────────────────────────────────────────────────────
POSTGRES_DDL = f"""
CREATE SCHEMA IF NOT EXISTS {SCHEMA};
CREATE TABLE IF NOT EXISTS {SET_TABLE} (
id SERIAL PRIMARY KEY,
source_code VARCHAR(50) NOT NULL,
occasion_tag VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (source_code, occasion_tag)
);
CREATE INDEX IF NOT EXISTS idx_outfit_set_source
ON {SET_TABLE}(source_code);
CREATE TABLE IF NOT EXISTS {ITEMS_TABLE} (
id SERIAL PRIMARY KEY,
outfit_id INT NOT NULL REFERENCES {SET_TABLE}(id) ON DELETE CASCADE,
target_code VARCHAR(50) NOT NULL,
role VARCHAR(30) NOT NULL,
rank SMALLINT NOT NULL DEFAULT 1,
score SMALLINT DEFAULT 0,
reason TEXT DEFAULT '',
is_pinned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_outfit_items_outfit_id
ON {ITEMS_TABLE}(outfit_id);
CREATE INDEX IF NOT EXISTS idx_outfit_items_target
ON {ITEMS_TABLE}(target_code);
"""
def run_sqlite():
"""Tạo bảng trên SQLite local."""
sqlite_path = os.getenv(
"SQLITE_PATH",
os.path.join(os.path.dirname(__file__), "..", "canifa_ai_dump.sqlite")
)
sqlite_path = os.path.abspath(sqlite_path)
import sqlite3
conn = sqlite3.connect(sqlite_path)
try:
conn.executescript(SQLITE_DDL)
conn.commit()
logger.info("[migrate] ✅ SQLite: ai_outfit_set + ai_outfit_items created at %s", sqlite_path)
print(f"[OK] SQLite tables created: {sqlite_path}")
except Exception as e:
logger.error("[migrate] SQLite error: %s", e)
print(f"[ERROR] SQLite: {e}")
finally:
conn.close()
def run_postgres():
"""Tạo bảng trên Postgres qua pool_wrapper."""
# Thêm thư mục backend vào sys.path để import được common.*
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)
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
# Chạy từng statement riêng để tránh lỗi "can't execute multiple commands"
for stmt in POSTGRES_DDL.split(";"):
stmt = stmt.strip()
if stmt:
cur.execute(stmt)
conn.commit()
cur.close()
logger.info("[migrate] ✅ Postgres: ai_outfit_set + ai_outfit_items created")
print("[OK] Postgres tables created: dashboard_canifa.ai_outfit_set + dashboard_canifa.ai_outfit_items")
except Exception as e:
if conn:
conn.rollback()
logger.error("[migrate] Postgres error: %s", e)
print(f"[ERROR] Postgres: {e}")
finally:
if conn:
conn.close()
def run():
"""Entry point: tự chọn backend theo USE_LOCAL_SQLITE."""
logging.basicConfig(level=logging.INFO)
if USE_SQLITE:
print("[migrate] Mode: SQLite (USE_LOCAL_SQLITE=true)")
run_sqlite()
else:
print("[migrate] Mode: Postgres (USE_LOCAL_SQLITE=false)")
run_postgres()
if __name__ == "__main__":
run()
"""
migrate_002_gender_rules.py
────────────────────────────────────────────────────────────
Migration:
1. Thêm cột gender_target vào chatbot_fashion_rules
2. Update existing rules → gender_target = 'all'
3. TRUNCATE + seed lại ~150 rules đầy đủ 5 demographic
Run:
python backend/database/migrate/migrate_002_gender_rules.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)
SCHEMA = "dashboard_canifa"
TABLE = f"{SCHEMA}.chatbot_fashion_rules"
# ──────────────────────────────────────────────────────────────────────────────
# 150 rules — 5 demographic × 4-5 dịp × top→bottom/outer
# gender_target: 'nu' | 'nam' | 'unisex' | 'be_gai' | 'be_trai' | 'all'
# ──────────────────────────────────────────────────────────────────────────────
RULES = [
# ══════════════════════════════════════════════════════════════════
# 👩 NỮ
# ══════════════════════════════════════════════════════════════════
# di_lam
("nu", "Blouse", "di_lam", "bottom", "Quần âu", "Blouse + Quần âu: bộ đôi công sở thanh lịch cho nữ"),
("nu", "Blouse", "di_lam", "bottom", "Chân váy", "Blouse + Chân váy: feminine style cho văn phòng"),
("nu", "Blouse", "di_lam", "outerwear", "Blazer", "Blazer phủ ngoài Blouse: smart casual, chuyên nghiệp"),
("nu", "Blouse", "di_lam", "outerwear", "Cardigan", "Cardigan nhẹ nhàng kết hợp Blouse: tinh tế công sở"),
("nu", "Áo Sơ mi", "di_lam", "bottom", "Quần âu", "Sơ mi + Quần âu: classic combo di làm"),
("nu", "Áo Sơ mi", "di_lam", "bottom", "Chân váy", "Sơ mi + Chân váy bút chì: modern minimal"),
("nu", "Áo Sơ mi", "di_lam", "outerwear", "Blazer", "Blazer kết hợp sơ mi: professional look"),
("nu", "Áo kiểu", "di_lam", "bottom", "Quần âu", "Áo kiểu + Quần âu: feminine công sở"),
("nu", "Áo kiểu", "di_lam", "bottom", "Chân váy", "Áo kiểu + Chân váy: nữ tính, thanh lịch"),
("nu", "Áo Polo", "di_lam", "bottom", "Quần âu", "Áo Polo nữ + Quần âu: smart casual"),
("nu", "Áo Polo", "di_lam", "bottom", "Chân váy", "Áo Polo + Chân váy: business casual"),
("nu", "Quần âu", "di_lam", "top", "Blouse", "Quần âu + Blouse: combo chuẩn công sở nữ"),
("nu", "Quần âu", "di_lam", "top", "Áo Sơ mi", "Quần âu + Sơ mi: clean look văn phòng"),
("nu", "Chân váy", "di_lam", "top", "Blouse", "Chân váy + Blouse: feminine office style"),
("nu", "Chân váy", "di_lam", "top", "Áo Sơ mi", "Chân váy + Sơ mi: versatile, thanh lịch"),
("nu", "Chân váy", "di_lam", "outerwear", "Blazer", "Chân váy + Blazer: power look cho nữ"),
# di_choi
("nu", "Áo phông", "di_choi", "bottom", "Quần jean", "Áo phông + Quần jean: combo quốc dân"),
("nu", "Áo phông", "di_choi", "bottom", "Quần soóc", "Áo phông + Quần soóc: năng động, trẻ trung"),
("nu", "Áo phông", "di_choi", "bottom", "Chân váy", "Áo phông + Chân váy: cô gái cute"),
("nu", "Áo phông", "di_choi", "outerwear", "Áo khoác gió", "Áo khoác gió nhẹ: mix casual hoàn hảo"),
("nu", "Áo kiểu", "di_choi", "bottom", "Quần jean", "Áo kiểu + Quần jean: trendy dạo phố"),
("nu", "Áo kiểu", "di_choi", "bottom", "Quần soóc", "Áo kiểu + Quần soóc: hot girl combo"),
("nu", "Áo kiểu", "di_choi", "bottom", "Chân váy", "Áo kiểu + Chân váy: chic, quyến rũ"),
("nu", "Áo nỉ", "di_choi", "bottom", "Quần jean", "Áo nỉ + Quần jean: casual chill mùa lạnh"),
("nu", "Quần jean", "di_choi", "top", "Áo phông", "Quần jean + Áo phông: timeless classic"),
("nu", "Quần jean", "di_choi", "top", "Áo kiểu", "Quần jean + Áo kiểu: balance cô gái"),
("nu", "Quần soóc", "di_choi", "top", "Áo phông", "Quần soóc + Áo phông: hè năng động"),
("nu", "Chân váy", "di_choi", "top", "Áo phông", "Chân váy + Áo phông: casual feminine"),
("nu", "Váy liền", "di_choi", "outerwear", "Áo khoác gió", "Váy liền + Áo khoác gió: airy, thời trang"),
# du_lich
("nu", "Áo hai dây", "du_lich", "bottom", "Chân váy", "Áo 2 dây + Chân váy: resort look tuyệt vời"),
("nu", "Áo hai dây", "du_lich", "bottom", "Quần soóc", "Áo 2 dây + Quần soóc: biển mùa hè"),
("nu", "Áo phông", "du_lich", "bottom", "Chân váy", "Áo phông + Chân váy: du lịch thoải mái"),
("nu", "Áo phông", "du_lich", "bottom", "Quần soóc", "Áo phông + Quần soóc: dạo biển năng động"),
("nu", "Áo phông", "du_lich", "outerwear", "Áo khoác gió", "Áo khoác gió: bảo vệ khỏi nắng và gió biển"),
("nu", "Váy liền", "du_lich", "outerwear", "Áo khoác gió", "Váy liền + Áo khoác: thanh lịch du lịch"),
("nu", "Chân váy", "du_lich", "top", "Áo hai dây", "Chân váy + Áo 2 dây: resort combo chuẩn"),
("nu", "Chân váy", "du_lich", "top", "Áo phông", "Chân váy + Áo phông: du lịch thư giãn"),
# mac_nha
("nu", "Áo phông", "mac_nha", "bottom", "Quần mặc nhà", "Áo phông + Quần mặc nhà: thoải mái tại nhà"),
("nu", "Áo phông", "mac_nha", "bottom", "Quần đùi cotton", "Áo phông + Quần đùi: chill ngày hè"),
("nu", "Áo hai dây", "mac_nha", "bottom", "Quần mặc nhà", "Áo 2 dây + Quần mặc nhà: nhẹ nhàng, mát mẻ"),
("nu", "Áo hai dây", "mac_nha", "bottom", "Quần đùi cotton", "Áo 2 dây + Quần đùi: summer lounge"),
("nu", "Quần mặc nhà", "mac_nha", "top", "Áo phông", "Quần mặc nhà + Áo phông: bộ nhà cơ bản"),
# ══════════════════════════════════════════════════════════════════
# 👨 NAM
# ══════════════════════════════════════════════════════════════════
# di_lam
("nam", "Áo Sơ mi", "di_lam", "bottom", "Quần khaki", "Sơ mi + Quần khaki: smart casual văn phòng nam"),
("nam", "Áo Sơ mi", "di_lam", "bottom", "Quần âu", "Sơ mi + Quần âu: formal business look"),
("nam", "Áo Sơ mi", "di_lam", "outerwear", "Blazer", "Sơ mi + Blazer: chuyên nghiệp tuyệt đối"),
("nam", "Áo Polo", "di_lam", "bottom", "Quần khaki", "Áo Polo + Quần khaki: business casual nam"),
("nam", "Áo Polo", "di_lam", "bottom", "Quần âu", "Áo Polo + Quần âu: lịch sự thoải mái"),
("nam", "Áo Polo", "di_lam", "outerwear", "Blazer", "Áo Polo + Blazer: elevated casual"),
("nam", "Quần khaki", "di_lam", "top", "Áo Sơ mi", "Quần khaki + Sơ mi: chuẩn công sở nam"),
("nam", "Quần khaki", "di_lam", "top", "Áo Polo", "Quần khaki + Áo Polo: smart casual dễ mặc"),
("nam", "Quần khaki", "di_lam", "outerwear", "Cardigan", "Quần khaki + Cardigan: prep style"),
("nam", "Quần âu", "di_lam", "top", "Áo Sơ mi", "Quần âu + Sơ mi: business professional"),
("nam", "Quần âu", "di_lam", "top", "Áo Polo", "Quần âu + Áo Polo: formal casual balance"),
# di_choi
("nam", "Áo phông", "di_choi", "bottom", "Quần jean", "Áo phông + Quần jean: quốc dân streetwear"),
("nam", "Áo phông", "di_choi", "bottom", "Quần soóc", "Áo phông + Quần soóc: hè năng động"),
("nam", "Áo phông", "di_choi", "outerwear", "Áo khoác gió", "Áo khoác gió: street style chill"),
("nam", "Áo nỉ", "di_choi", "bottom", "Quần jean", "Áo nỉ + Quần jean: casual mùa lạnh"),
("nam", "Áo nỉ có mũ", "di_choi", "bottom", "Quần jean", "Hoodie + Quần jean: streetwear cơ bản"),
("nam", "Áo nỉ có mũ", "di_choi", "bottom", "Quần soóc", "Hoodie + Quần soóc: athleisure look"),
("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", "top", "Áo nỉ có mũ", "Quần jean + Hoodie: street icon"),
("nam", "Quần soóc", "di_choi", "top", "Áo phông", "Quần soóc + Áo phông: chill ngày hè"),
# du_lich
("nam", "Áo Polo", "du_lich", "bottom", "Quần khaki", "Áo Polo + Quần khaki: du lịch lịch sự"),
("nam", "Áo Polo", "du_lich", "bottom", "Quần soóc", "Áo Polo + Quần soóc: resort casual"),
("nam", "Áo phông", "du_lich", "bottom", "Quần khaki", "Áo phông + Quần khaki: utility travel"),
("nam", "Áo phông", "du_lich", "bottom", "Quần soóc", "Áo phông + Quần soóc: biển hè"),
("nam", "Áo phông", "du_lich", "outerwear", "Áo khoác gió", "Áo khoác gió: chống nắng du lịch"),
("nam", "Quần khaki", "du_lich", "top", "Áo phông", "Quần khaki + Áo phông: adventure ready"),
("nam", "Quần khaki", "du_lich", "top", "Áo Polo", "Quần khaki + Áo Polo: smart traveler"),
# the_thao
("nam", "Áo phông", "the_thao", "bottom", "Quần thể thao", "Áo phông + Quần thể thao: gym basic"),
("nam", "Áo phông", "the_thao", "bottom", "Quần soóc", "Áo phông + Quần soóc: running outfit"),
# ══════════════════════════════════════════════════════════════════
# 🔁 UNISEX (Adults)
# ══════════════════════════════════════════════════════════════════
# hang_ngay
("unisex", "Áo phông", "hang_ngay", "bottom", "Quần jean", "Áo phông + Quần jean: universal everyday"),
("unisex", "Áo phông", "hang_ngay", "bottom", "Quần dài", "Áo phông + Quần dài: casual versatile"),
("unisex", "Áo Sơ mi", "hang_ngay", "bottom", "Quần jean", "Sơ mi oversize + Quần jean: essential look"),
("unisex", "Áo Sơ mi", "hang_ngay", "bottom", "Quần dài", "Sơ mi + Quần dài: clean minimal"),
("unisex", "Quần jean", "hang_ngay", "top", "Áo phông", "Quần jean + Áo phông: always works"),
("unisex", "Quần jean", "hang_ngay", "top", "Áo Sơ mi", "Quần jean + Sơ mi: casual elevated"),
# the_thao
("unisex", "Áo phông", "the_thao", "bottom", "Quần thể thao", "Áo phông + Quần thể thao: workout ready"),
("unisex", "Áo phông", "the_thao", "bottom", "Quần leggings", "Áo phông + Leggings: athleisure"),
("unisex", "Quần thể thao", "the_thao", "top", "Áo phông", "Quần thể thao + Áo phông: sport set"),
# di_choi
("unisex", "Áo nỉ có mũ", "di_choi", "bottom", "Quần jean", "Hoodie + Quần jean: street essential"),
("unisex", "Áo phông", "di_choi", "bottom", "Quần jean", "Áo phông + Quần jean: effortless casual"),
("unisex", "Áo phông", "di_choi", "outerwear", "Áo khoác gió", "Áo khoác gió + Áo phông: streetwear layer"),
# ══════════════════════════════════════════════════════════════════
# 👧 BÉ GÁI
# ══════════════════════════════════════════════════════════════════
# hang_ngay
("be_gai", "Áo phông", "hang_ngay", "bottom", "Quần leggings", "Áo phông bé gái + Leggings: thoải mái năng động"),
("be_gai", "Áo phông", "hang_ngay", "bottom", "Quần nỉ", "Áo phông + Quần nỉ: ấm áp ngày lạnh"),
("be_gai", "Áo kiểu", "hang_ngay", "bottom", "Quần leggings", "Áo kiểu cute + Leggings: dễ thương hàng ngày"),
("be_gai", "Quần leggings", "hang_ngay", "top", "Áo phông", "Leggings + Áo phông: bộ hàng ngày tiện lợi"),
# di_choi
("be_gai", "Áo kiểu", "di_choi", "bottom", "Chân váy", "Áo kiểu + Chân váy: bé gái xinh xắn đi chơi"),
("be_gai", "Áo phông", "di_choi", "bottom", "Chân váy", "Áo phông + Chân váy: cute girl style"),
("be_gai", "Áo phông", "di_choi", "bottom", "Quần soóc", "Áo phông + Quần soóc: vui chơi ngoài trời"),
("be_gai", "Chân váy", "di_choi", "top", "Áo phông", "Chân váy + Áo phông: adorable combination"),
("be_gai", "Chân váy", "di_choi", "top", "Áo kiểu", "Chân váy + Áo kiểu: đặc biệt ngày đi chơi"),
("be_gai", "Váy liền", "di_choi", "outerwear", "Áo khoác gió","Váy liền + Áo khoác: complete cute look"),
# du_lich
("be_gai", "Áo phông", "du_lich", "bottom", "Chân váy", "Áo phông + Chân váy: du lịch dễ thương"),
("be_gai", "Áo phông", "du_lich", "bottom", "Quần soóc", "Áo phông + Quần soóc: biển hè cho bé"),
("be_gai", "Váy liền", "du_lich", "outerwear", "Áo khoác gió", "Váy liền + Áo khoác: resort look cho bé gái"),
# ══════════════════════════════════════════════════════════════════
# 👦 BÉ TRAI
# ══════════════════════════════════════════════════════════════════
# hang_ngay
("be_trai", "Áo phông", "hang_ngay", "bottom", "Quần soóc", "Áo phông bé trai + Quần soóc: năng động hàng ngày"),
("be_trai", "Áo phông", "hang_ngay", "bottom", "Quần nỉ", "Áo phông + Quần nỉ: ấm trong ngày lạnh"),
("be_trai", "Áo nỉ", "hang_ngay", "bottom", "Quần dài", "Áo nỉ + Quần dài: casual ngày thường"),
("be_trai", "Quần soóc", "hang_ngay", "top", "Áo phông", "Quần soóc + Áo phông: bộ hàng ngày năng động"),
# the_thao
("be_trai", "Áo phông", "the_thao", "bottom", "Quần thể thao", "Áo phông + Quần thể thao: tập luyện cho bé"),
("be_trai", "Áo phông", "the_thao", "bottom", "Quần leggings", "Áo phông + Leggings co giãn: vận động tối đa"),
("be_trai", "Áo phông", "the_thao", "bottom", "Quần soóc", "Áo phông + Quần soóc: chạy nhảy tự do"),
# du_lich
("be_trai", "Áo phông", "du_lich", "bottom", "Quần dài", "Áo phông + Quần dài: du lịch thoải mái"),
("be_trai", "Áo nỉ", "du_lich", "bottom", "Quần dài", "Áo nỉ + Quần dài: dã ngoại an toàn"),
("be_trai", "Áo phông", "du_lich", "outerwear", "Áo khoác gió", "Áo khoác gió chống nắng: du lịch cho bé trai"),
# ══════════════════════════════════════════════════════════════════
# ⚪ ALL — Rules không phân biệt giới tính (outer common, dịp phụ)
# ══════════════════════════════════════════════════════════════════
("all", "Áo phông", "mac_nha", "bottom", "Quần mặc nhà", "Universal: Áo phông + Quần mặc nhà"),
("all", "Áo phông", "mac_nha", "bottom", "Quần đùi cotton","Universal: Áo phông + Quần đùi chill"),
("all", "Áo khoác gió", "du_lich", "outerwear", "Áo phông", "Áo khoác gió phủ ngoài: du lịch mọi giới"),
("all", "Áo khoác gió", "di_choi", "outerwear", "Áo phông", "Áo khoác gió + Áo phông: street layer"),
("all", "Cardigan", "di_lam", "outerwear", "Áo Sơ mi", "Cardigan + Áo Sơ mi: layering công sở"),
("all", "Cardigan", "hang_ngay","outerwear", "Áo phông", "Cardigan + Áo phông: casual layer"),
("all", "Áo khoác nỉ có mũ", "di_choi", "outerwear", "Áo phông", "Hoodie mặc ngoài: layering streetwear"),
]
def run():
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
# 1. Add column if not exists
cur.execute(f"""
ALTER TABLE {TABLE}
ADD COLUMN IF NOT EXISTS gender_target VARCHAR(20) DEFAULT 'all'
""")
logger.info("[migrate002] ✅ Column gender_target added/exists")
# 2. Update existing rows to 'all'
cur.execute(f"UPDATE {TABLE} SET gender_target = 'all' WHERE gender_target IS NULL OR gender_target = ''")
logger.info("[migrate002] ✅ Existing rows updated to gender_target='all'")
# 3. Insert new gender-specific rules (ON CONFLICT DO NOTHING)
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()
total = len(RULES)
logger.info("[migrate002] ✅ Inserted %d/%d new gender rules", inserted, total)
print(f"[OK] Migration 002 done: +{inserted} new rules ({total} total in batch)")
except Exception as e:
if conn:
conn.rollback()
logger.error("[migrate002] Error: %s", e)
print(f"[ERROR] {e}")
finally:
if conn:
conn.close()
if __name__ == "__main__":
run()
"""
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
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 = [
# Á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
import os
import 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()
import os
import sys
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 = [
# Bộ mặc nhà (nguyên set) -> Phối với phụ kiện hoặc outerwear
("all", "Bộ mặc nhà", "mac_nha", "accessory", "Tất", "Bộ mặc nhà + Tất: Giữ ấm bàn chân khi ngủ"),
("all", "Bộ mặc nhà", "mac_nha", "outerwear", "Áo khoác gió", "Bộ mặc nhà + Áo khoác gió: Mặc ngoài khi ra khỏi phòng"),
# Quần mặc nhà -> Phối với Áo mặc nhà hoặc Áo phông
("all", "Quần mặc nhà", "mac_nha", "top", "Áo mặc nhà", "Quần mặc nhà + Áo mặc nhà: Nguyên set thoải mái"),
("all", "Quần mặc nhà", "mac_nha", "top", "Áo phông", "Quần mặc nhà + Áo phông: Đơn giản, thoải mái"),
# Áo mặc nhà Bé Trai / Bé Gái / Nữ (Bổ sung thêm giới tính)
("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_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"),
("nu", "Áo mặc nhà", "mac_nha", "bottom", "Quần mặc nhà", "Áo mặc nhà + Quần lụa/cotton lửng cho nữ"),
# Áo khoác chống nắng -> Phối chống nắng cơ bản lớp ngoài
("all", "Áo khoác chống nắng", "di_choi", "top", "Áo phông", "Khoác chống nắng ngoài Áo phông"),
("all", "Áo khoác chống nắng", "di_choi", "bottom", "Quần jean", "Khoác chống nắng + Quần jean năng động"),
# Tất -> Phụ kiện (nếu Tất làm món chính)
("all", "Tất", "hang_ngay", "bottom", "Quần soóc", "Tất + Quần soóc thao năng động"),
("all", "Tất", "the_thao", "bottom", "Quần thể thao", "Tất + Quần thể thao chuyên dụng"),
]
def run():
import os
import sys
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, backend_dir)
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} 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_006_ao_phong_full.py
Seed FULL rules cho Áo phông (nữ / nam / unisex) × 4 dịp × tất cả product_line phù hợp
"""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__) + "/../..")
RULES = []
# ─── NỮ ──────────────────────────────────────────────────────────────────────
def r(anchor, gender, occ, role, target, reason):
RULES.append((anchor, gender, occ, role, target, reason))
# Áo phông nữ - Đi chơi / dạo phố
r("Áo phông","nu","di_choi","bottom","Quần jean","Áo phông + Quần jean: combo quốc dân đi chơi cuối tuần")
r("Áo phông","nu","di_choi","bottom","Quần soóc","Áo phông + Quần soóc: thoải mái, trẻ trung")
r("Áo phông","nu","di_choi","bottom","Chân váy","Áo phông + Chân váy: mix style nữ tính")
r("Áo phông","nu","di_choi","bottom","Quần nỉ","Áo phông + Quần nỉ: sporty, casual")
r("Áo phông","nu","di_choi","bottom","Quần dài","Áo phông + Quần dài: đơn giản, thanh lịch")
r("Áo phông","nu","di_choi","bottom","Váy liền","Áo phông tucked-in + Váy liền: style layering")
r("Áo phông","nu","di_choi","outerwear","Áo khoác chống nắng","Áo phông + Áo chống nắng: bảo vệ da khi ra ngoài")
r("Áo phông","nu","di_choi","outerwear","Áo khoác gió","Áo phông + Jacket gió: streetwear year-round")
r("Áo phông","nu","di_choi","outerwear","Áo khoác dáng ngắn","Áo phông + Khoác dáng ngắn: trendy")
r("Áo phông","nu","di_choi","outerwear","Áo len","Áo phông + Áo len: layering mùa thu")
r("Áo phông","nu","di_choi","outerwear","Cardigan","Áo phông + Cardigan: nhẹ nhàng, nữ tính")
r("Áo phông","nu","di_choi","accessory","Tất","Tất phối tông để hoàn thiện look")
r("Áo phông","nu","di_choi","accessory","Túi xách","Túi xách unisex hoàn thiện outfit đi chơi")
r("Áo phông","nu","di_choi","accessory","Mũ","Mũ che nắng/phối style khi đi chơi")
# Áo phông nữ - Đi làm công sở
r("Áo phông","nu","cong_so","bottom","Quần dài","Áo phông tuck-in + Quần dài: lịch sự, hiện đại")
r("Áo phông","nu","cong_so","bottom","Chân váy","Áo phông + Chân váy midi: nữ tính công sở")
r("Áo phông","nu","cong_so","bottom","Quần Khaki","Áo phông + Quần khaki: smart casual")
r("Áo phông","nu","cong_so","bottom","Quần jean","Áo phông + Jean công sở: casual Friday")
r("Áo phông","nu","cong_so","outerwear","Blazer","Áo phông + Blazer: smart casual đỉnh cao")
r("Áo phông","nu","cong_so","outerwear","Cardigan","Áo phông + Cardigan: thanh lịch văn phòng")
r("Áo phông","nu","cong_so","outerwear","Áo len gilet","Áo phông + Gilet len: phong cách công sở mùa lạnh")
r("Áo phông","nu","cong_so","accessory","Tất","Tất mỏng phối văn phòng")
# Áo phông nữ - Du lịch
r("Áo phông","nu","du_lich","bottom","Quần soóc","Áo phông + Shorts: thoải mái du lịch")
r("Áo phông","nu","du_lich","bottom","Quần jean","Áo phông + Jean: versatile khi đi du lịch")
r("Áo phông","nu","du_lich","bottom","Quần nỉ","Áo phông + Quần nỉ: di chuyển thoải mái xa")
r("Áo phông","nu","du_lich","bottom","Chân váy","Áo phông + Chân váy: nhẹ nhàng du lịch biển")
r("Áo phông","nu","du_lich","outerwear","Áo khoác chống nắng","Chống nắng hiệu quả khi khám phá ngoài trời")
r("Áo phông","nu","du_lich","outerwear","Áo khoác gió","Áo khoác gió nhẹ, gọn gàng khi du lịch")
r("Áo phông","nu","du_lich","outerwear","Áo khoác chần bông","Ấm áp khi đi du lịch miền lạnh")
r("Áo phông","nu","du_lich","accessory","Tất","Tất năng động phù hợp đi du lịch")
r("Áo phông","nu","du_lich","accessory","Túi xách","Túi xách gọn nhẹ tiện dụng du lịch")
r("Áo phông","nu","du_lich","accessory","Mũ","Mũ bảo vệ khi tham quan ngoài nắng")
# Áo phông nữ - Ở nhà
r("Áo phông","nu","mac_nha","bottom","Quần mặc nhà","Áo phông + Quần mặc nhà: ở nhà thoải mái")
r("Áo phông","nu","mac_nha","bottom","Quần nỉ","Áo phông + Quần nỉ: casual homewear")
r("Áo phông","nu","mac_nha","bottom","Pyjama","Áo phông + Pyjama: ngủ thoải mái")
# ─── NAM ─────────────────────────────────────────────────────────────────────
# Áo phông nam - Đi chơi
r("Áo phông","nam","di_choi","bottom","Quần jean","Áo phông + Jean: combo đi chơi kinh điển nam")
r("Áo phông","nam","di_choi","bottom","Quần soóc","Áo phông + Shorts: nam tính, năng động")
r("Áo phông","nam","di_choi","bottom","Quần nỉ","Áo phông + Quần nỉ: sporty casual nam")
r("Áo phông","nam","di_choi","bottom","Quần Khaki","Áo phông + Khaki: casual smart")
r("Áo phông","nam","di_choi","bottom","Quần dài","Áo phông + Quần dài đứng: gọn gàng")
r("Áo phông","nam","di_choi","outerwear","Áo khoác gió","Áo phông + Jacket gió: streetwear nam")
r("Áo phông","nam","di_choi","outerwear","Áo nỉ có mũ","Áo phông layered cùng hoodie: urban style")
r("Áo phông","nam","di_choi","outerwear","Áo khoác dáng ngắn","Khoác dáng ngắn tạo điểm nhấn")
r("Áo phông","nam","di_choi","outerwear","Áo khoác chống nắng","Chống nắng tiện lợi cho nam")
r("Áo phông","nam","di_choi","accessory","Tất","Tất phối đồng bộ với giày/sneaker")
r("Áo phông","nam","di_choi","accessory","Mũ","Mũ snapback/5 panel phối streetwear")
r("Áo phông","nam","di_choi","accessory","Túi xách","Túi tote/messenger: thực dụng khi ra ngoài")
# Áo phông nam - Đi làm
r("Áo phông","nam","cong_so","bottom","Quần Khaki","Áo phông + Quần khaki: casual Friday văn phòng")
r("Áo phông","nam","cong_so","bottom","Quần jean","Áo phông + Jean: công sở casual")
r("Áo phông","nam","cong_so","bottom","Quần dài","Áo phông + Quần dài: neat và lịch sự")
r("Áo phông","nam","cong_so","outerwear","Blazer","Áo phông + Blazer: smart casual hiện đại")
r("Áo phông","nam","cong_so","outerwear","Áo len","Áo phông layered + Len: business casual mùa lạnh")
r("Áo phông","nam","cong_so","accessory","Tất","Tất phẳng phối công sở")
# Áo phông nam - Du lịch
r("Áo phông","nam","du_lich","bottom","Quần soóc","Áo phông + Shorts: du lịch biển năng động")
r("Áo phông","nam","du_lich","bottom","Quần jean","Áo phông + Jean: versatile du lịch")
r("Áo phông","nam","du_lich","bottom","Quần nỉ","Áo phông + Nỉ: di chuyển thoải mái")
r("Áo phông","nam","du_lich","outerwear","Áo khoác chống nắng","Chống nắng gọn nhẹ du lịch ngoài trời")
r("Áo phông","nam","du_lich","outerwear","Áo khoác gió","Jacket gió nhẹ, gấp gọn du lịch")
r("Áo phông","nam","du_lich","outerwear","Áo khoác chần bông","Ấm khi du lịch miền núi/biển đêm")
r("Áo phông","nam","du_lich","accessory","Tất","Tất du lịch năng động")
r("Áo phông","nam","du_lich","accessory","Mũ","Mũ bảo vệ ngoài trời")
r("Áo phông","nam","du_lich","accessory","Túi xách","Túi crossbody gọn nhẹ du lịch")
# Áo phông nam - Ở nhà
r("Áo phông","nam","mac_nha","bottom","Quần mặc nhà","Áo phông + Quần mặc nhà: ở nhà thư giãn")
r("Áo phông","nam","mac_nha","bottom","Quần nỉ","Áo phông + Nỉ: thoải mái tối về nhà")
# ─── UNISEX ───────────────────────────────────────────────────────────────────
r("Áo phông","unisex","di_choi","bottom","Quần jean","Áo phông + Jean: phóng khoáng unisex")
r("Áo phông","unisex","di_choi","bottom","Quần soóc","Áo phông + Shorts: casual unisex")
r("Áo phông","unisex","di_choi","bottom","Quần nỉ","Áo phông + Nỉ: sporty unisex")
r("Áo phông","unisex","di_choi","outerwear","Áo khoác gió","Jacket gió unisex streetwear")
r("Áo phông","unisex","di_choi","outerwear","Áo khoác dáng ngắn","Khoác ngắn unisex trend")
r("Áo phông","unisex","di_choi","accessory","Tất","Tất phối unisex")
r("Áo phông","unisex","di_choi","accessory","Mũ","Mũ unisex streetstyle")
r("Áo phông","unisex","du_lich","bottom","Quần soóc","Shorts du lịch unisex")
r("Áo phông","unisex","du_lich","bottom","Quần jean","Jean du lịch unisex")
r("Áo phông","unisex","du_lich","outerwear","Áo khoác chống nắng","Chống nắng unisex")
r("Áo phông","unisex","du_lich","accessory","Mũ","Mũ du lịch unisex")
TABLE = "dashboard_canifa.chatbot_fashion_rules"
def run():
import os
import sys
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, backend_dir)
from common.pool_wrapper import get_pooled_connection_compat
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
upserted = 0
for anchor, gender, occ, role, target, reason in RULES:
cur.execute(f"""
INSERT INTO {TABLE} (anchor_category, gender_target, occasion_tag, match_role, target_category, ai_reason)
VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING
""", (anchor, gender, occ, role, target, reason))
if cur.rowcount > 0:
upserted += 1
conn.commit()
cur.close()
print(f"[OK] migrate_006 done: +{upserted} rules upserted ({len(RULES)} total attempted)")
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
import 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
import 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
import 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()
from worker.stylist_engine import StylistEngine
def test_engine():
engine = StylistEngine()
print("Catalog loaded. Finding adult product...")
catalog = engine._get_catalog()
# find an adult shirt
adult_product = None
for p in catalog:
gender = p.get('gender', '').lower()
if gender in ['nam', 'nữ', 'nu', 'women', 'men']:
adult_product = p
break
if not adult_product:
print("No adult product found.")
return
code = adult_product['code']
print(f"Testing Code: {code} | Name: {adult_product['name']} | Gender: {adult_product['gender']}")
print("\n--- 1. Testing 'compute_dynamic_rule_matches' (AI MATCHES) ---")
matches = engine.compute_dynamic_rule_matches(code)
if not matches:
print("No AI matches found.")
else:
for occ, roles in matches.items():
for role, items in roles.items():
for item in items[:2]:
item_gender = item.get('gender', 'unknown').lower()
item_name = item.get('name', 'unknown')
if 'bé' in item_name.lower() or 'bé' in item_gender or 'be' in item_gender:
print(f" [AI-MATCH] FOUND KID: {item['code']} - {item_name} (gender: {item_gender})")
else:
print(f" [AI-MATCH] OK: {item['code']} - {item_name} (gender: {item_gender})")
print("\n--- 2. Testing 'compute_super_classifications_sql' (SUPER CLASSIFICATIONS) ---")
classifications = engine.compute_super_classifications_sql(code)
if not classifications:
print("No classification matches found.")
else:
for group, groups_dict in classifications.items():
for key, items in groups_dict.items():
for item in items[:2]:
item_name = item.get('name', 'unknown')
if 'bé' in item_name.lower():
print(f" [SUPER-CLASS] FOUND KID: {item['code']} - {item_name}")
else:
print(f" [SUPER-CLASS] OK: {item['code']} - {item_name}")
if __name__ == "__main__":
test_engine()
import sys
import os
import sqlite3
import logging
# Thêm thư mục backend vào sys.path để có thể import được common, config
backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
from common.starrocks_connection import get_db_connection
from common.constants import SQLITE_DB_PATH
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def migrate_similar_items():
logger.info("Đang kết nối tới StarRocks...")
sr_db = get_db_connection()
query = """
SELECT magento_ref_code, similar_items
FROM test_db.magento_product_dimension_with_text_embedding
WHERE similar_items IS NOT NULL AND similar_items != ''
"""
logger.info("Đang tải dữ liệu similar_items từ StarRocks...")
rows = sr_db.execute_query(query)
if not rows:
logger.warning("Không tìm thấy dữ liệu similar_items nào trong StarRocks!")
return
logger.info(f"Đã tải {len(rows)} sản phẩm có similar_items từ StarRocks. Chuẩn bị migrate vào SQLite...")
logger.info(f"Đang kết nối tới SQLite: {SQLITE_DB_PATH}")
if not os.path.exists(SQLITE_DB_PATH):
logger.error(f"Không tìm thấy file SQLite tại {SQLITE_DB_PATH}")
return
conn = sqlite3.connect(SQLITE_DB_PATH)
cursor = conn.cursor()
update_count = 0
cursor.execute("BEGIN TRANSACTION")
try:
for row in rows:
ref_code = row['magento_ref_code']
similar_items = row['similar_items']
# CẬP NHẬT TRỰC TIẾP, KHÔNG PARSE, KHÔNG LÀM GÌ CẢ
cursor.execute(
"""
UPDATE sr__test_db__magento_product_dimension_with_text_embedding
SET similar_items = ?
WHERE magento_ref_code = ?
""",
(similar_items, ref_code)
)
update_count += cursor.rowcount
conn.commit()
logger.info(f"✅ Migrate thành công! Đã copy trực tiếp {update_count} dòng sang SQLite.")
except Exception as e:
conn.rollback()
logger.error(f"❌ Lỗi trong quá trình update SQLite: {e}")
finally:
conn.close()
if __name__ == "__main__":
migrate_similar_items()
import os
import re
import sqlite3
db_path = r"C:\canifa-idea\chatbot-canifa-feedback\backend\database\canifa_local.sqlite"
conn = sqlite3.connect(db_path)
cur = conn.cursor()
def translate_query(query: str) -> str:
q = query
q = re.sub(r'"dashboard_canifa"\."([a-zA-Z0-9_]+)"', r'pg__dashboard_canifa__\1', q)
q = re.sub(r'"canifa_chat"\."([a-zA-Z0-9_]+)"', r'pg__canifa_chat__\1', q)
q = re.sub(r'"public"\."([a-zA-Z0-9_]+)"', r'pg__public__\1', q)
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)
q = re.sub(r'TRUNCATE TABLE\s+(pg__[a-zA-Z0-9_]+)(\s+CASCADE)?;?', r'DELETE FROM \1;', q, flags=re.IGNORECASE)
q = q.replace("::jsonb", "").replace("::uuid", "")
return q
def ensure_tables():
cur.execute("""
CREATE TABLE IF NOT EXISTS pg__dashboard_canifa__ultra_descriptions (
id INTEGER PRIMARY KEY,
internal_ref_code TEXT,
product_name TEXT,
product_image_url TEXT,
product_line TEXT,
description_data TEXT,
phase TEXT,
created_at TEXT,
updated_at TEXT,
status TEXT,
clean_description TEXT,
tags TEXT,
size_scale TEXT,
magento_ref_code TEXT,
base_ref_code TEXT,
embedding TEXT,
ai_matches TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS sr__test_db__magento_product_dimension_with_text_embedding (
internal_ref_code TEXT,
magento_ref_code TEXT,
product_color_code TEXT,
product_name TEXT,
color_code TEXT,
master_color TEXT,
product_color_name TEXT,
season_sale TEXT,
season TEXT,
style TEXT,
fitting TEXT,
size_scale TEXT,
graphic TEXT,
pattern TEXT,
weaving TEXT,
shape_detail TEXT,
form_neckline TEXT,
form_sleeve TEXT,
form_length TEXT,
form_waistline TEXT,
form_shoulderline TEXT,
material TEXT,
product_group TEXT,
product_line_vn TEXT,
unit_of_measure TEXT,
sale_price REAL,
original_price REAL,
discount_amount REAL,
material_group TEXT,
product_line_en TEXT,
age_by_product TEXT,
gender_by_product TEXT,
quantity_sold REAL,
is_new_product INTEGER,
product_image_url TEXT,
description_text TEXT,
product_image_url_thumbnail TEXT,
product_web_url TEXT,
product_web_material TEXT,
description_text_full TEXT,
tags TEXT,
similar_items TEXT, -- suggest_items removed (deprecated)
vector TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS pg__dashboard_canifa__chatbot_fashion_rules (
id INTEGER PRIMARY KEY,
anchor_category TEXT,
occasion_tag TEXT,
match_role TEXT,
target_category TEXT,
ai_reason TEXT,
gender_target TEXT
)
""")
conn.commit()
ensure_tables()
with open("migration_log.txt", "w", encoding="utf-8") as log:
for folder in ['postgres', 'starrocks']:
path = os.path.join(r"C:\canifa-idea\chatbot-canifa-feedback\backend\database", folder)
if not os.path.exists(path): continue
for file in os.listdir(path):
if file.endswith('.sql'):
filepath = os.path.join(path, file)
log.write(f"Reading {file}...\n")
log.flush()
with open(filepath, 'r', encoding='utf-8') as f:
buffer = ""
in_statement = False
for line in f:
if line.startswith("INSERT") or line.startswith("TRUNCATE") or line.startswith("DELETE"):
in_statement = True
buffer = line
elif in_statement:
buffer += line
if in_statement and line.strip().endswith(";"):
sql = translate_query(buffer)
try:
cur.execute(sql)
except Exception as e:
log.write(f"Error executing statement in {file}: {e}\n")
log.flush()
in_statement = False
buffer = ""
conn.commit()
conn.close()
with open("migration_log.txt", "a", encoding="utf-8") as log:
log.write("Migration complete.\n")
Reading canifa_chat.lead_flow_history.sql...
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Reading dashboard_canifa.activity_logs.sql...
Reading dashboard_canifa.admin_users.sql...
Error executing statement in dashboard_canifa.admin_users.sql: no such table: pg__dashboard_canifa__admin_users
Error executing statement in dashboard_canifa.admin_users.sql: no such table: pg__dashboard_canifa__admin_users
Reading dashboard_canifa.ai_outfit_tables.sql...
Reading dashboard_canifa.chatbot_fashion_rules.sql...
Error executing statement in dashboard_canifa.chatbot_fashion_rules.sql: no such table: pg__dashboard_canifa__chatbot_fashion_rules
Error executing statement in dashboard_canifa.chatbot_fashion_rules.sql: no such table: pg__dashboard_canifa__chatbot_fashion_rules
Reading dashboard_canifa.chat_history.sql...
Error executing statement in dashboard_canifa.chat_history.sql: no such table: pg__dashboard_canifa__chat_history
Error executing statement in dashboard_canifa.chat_history.sql: no such table: pg__dashboard_canifa__chat_history
Reading dashboard_canifa.desc_field_config.sql...
Error executing statement in dashboard_canifa.desc_field_config.sql: no such table: pg__dashboard_canifa__desc_field_config
Error executing statement in dashboard_canifa.desc_field_config.sql: no such table: pg__dashboard_canifa__desc_field_config
Reading dashboard_canifa.product_size_guide.sql...
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Reading dashboard_canifa.saved_reports.sql...
Error executing statement in dashboard_canifa.saved_reports.sql: no such table: pg__dashboard_canifa__saved_reports
Error executing statement in dashboard_canifa.saved_reports.sql: no such table: pg__dashboard_canifa__saved_reports
Reading dashboard_canifa.sql_trace_sessions.sql...
Error executing statement in dashboard_canifa.sql_trace_sessions.sql: no such table: pg__dashboard_canifa__sql_trace_sessions
Error executing statement in dashboard_canifa.sql_trace_sessions.sql: no such table: pg__dashboard_canifa__sql_trace_sessions
Reading dashboard_canifa.system_settings.sql...
Reading dashboard_canifa.ultra_descriptions.sql...
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Reading public.chatbot_fashion_rules.sql...
Error executing statement in public.chatbot_fashion_rules.sql: no such table: pg__public__chatbot_fashion_rules
Error executing statement in public.chatbot_fashion_rules.sql: no such table: pg__public__chatbot_fashion_rules
Reading public.prompt_rules.sql...
Error executing statement in public.prompt_rules.sql: no such table: pg__public__prompt_rules
Error executing statement in public.prompt_rules.sql: no such table: pg__public__prompt_rules
Reading test_db.magento_product_dimension_with_text_embedding.sql...
-- Migration: Add `size` column to ultra_descriptions
-- Purpose: Store filtered size table based on size_scale to reduce token usage
-- Date: 2026-05-05
-- Database: SQLite
-- 1. Add new column
ALTER TABLE pg__dashboard_canifa__ultra_descriptions ADD COLUMN size TEXT;
-- 2. Note: Use populate_size_column.py to fill this column
-- because SQLite lacks advanced string manipulation functions.
COMMIT;
"""
Populate `size` column in pg__dashboard_canifa__ultra_descriptions
with filtered size table based on size_scale.
"""
import json
import os
import sqlite3
import sys
sys.stdout.reconfigure(encoding='utf-8')
# Auto-detect DB path relative to this script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Try common DB filenames
possible_db_names = [
"canifa_ai_dump.sqlite",
"canifa_local.sqlite",
"canifa_dump.sqlite"
]
DB_PATH = None
for db_name in possible_db_names:
candidate = os.path.join(SCRIPT_DIR, db_name)
if os.path.exists(candidate):
DB_PATH = candidate
print(f"✅ Found DB: {DB_PATH}")
break
if not DB_PATH:
# Default to first option with debug
DB_PATH = os.path.join(SCRIPT_DIR, "canifa_ai_dump.sqlite")
print(f"⚠️ DB not auto-found. Will try: {DB_PATH}")
def parse_size_table_from_markdown(markdown: str) -> dict:
result = {}
lines = markdown.strip().split('\n')
for line in lines:
line = line.strip()
if line.startswith('|') and not line.startswith('|---'):
parts = [p.strip() for p in line.split('|')]
if len(parts) >= 4:
size = parts[1]
height = parts[2]
weight = parts[3]
result[size] = (height, weight)
return result
def build_filtered_table(size_table: dict, available_sizes: list) -> str:
if not size_table or not available_sizes:
return ""
lines = [
"| Size | Chiều cao (cm) | Cân nặng (kg) |",
"|------|----------------|---------------|"
]
for size in available_sizes:
size_upper = size.strip().upper()
for table_size, (height, weight) in size_table.items():
if table_size.upper() == size_upper:
lines.append(f"| {size} | {height} | {weight} |")
break
return '\n'.join(lines)
def main():
print(f"\n🔍 DB Path: {DB_PATH}")
print(f"📁 Script dir: {SCRIPT_DIR}")
print(f"📁 Current dir: {os.getcwd()}")
print(f"❌ Exists: {os.path.exists(DB_PATH)}\n")
if not os.path.exists(DB_PATH):
print("❌ Database not found!")
print("\nAvailable .sqlite files in script directory:")
for f in os.listdir(SCRIPT_DIR):
if f.endswith('.sqlite'):
print(f" - {f}")
print("\n👉 Please edit DB_PATH in this script to point to the correct file.")
return
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
# Ensure column exists
try:
cur.execute("ALTER TABLE pg__dashboard_canifa__ultra_descriptions ADD COLUMN size TEXT")
print("✅ Added 'size' column")
except sqlite3.OperationalError as e:
if "duplicate column" in str(e).lower():
print("ℹ️ Column 'size' already exists - will skip creation")
else:
print(f"❌ Error adding column: {e}")
return
# Fetch rows to process
cur.execute("""
SELECT id, description_data, size_scale
FROM pg__dashboard_canifa__ultra_descriptions
WHERE description_data IS NOT NULL
AND description_data != ''
AND size_scale IS NOT NULL
AND size_scale != ''
AND (size IS NULL OR size = '')
""")
rows = cur.fetchall()
total = len(rows)
print(f"📊 Found {total} rows to process")
if total == 0:
# Check if any rows have data already
cur.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ultra_descriptions")
total_rows = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ultra_descriptions WHERE size IS NOT NULL")
already_done = cur.fetchone()[0]
print(f"✅ No rows need processing. Total: {total_rows}, Already filled: {already_done}")
conn.close()
return
updated = 0
errors = 0
for row_id, desc_data_json, size_scale in rows:
try:
desc_data = json.loads(desc_data_json)
huong_dan_size = desc_data.get('huong_dan_size', '')
if not huong_dan_size:
continue
available_sizes = [s.strip() for s in size_scale.split('|') if s.strip()]
if not available_sizes:
continue
size_table = parse_size_table_from_markdown(huong_dan_size)
if not size_table:
continue
filtered_table = build_filtered_table(size_table, available_sizes)
if filtered_table:
cur.execute(
"UPDATE pg__dashboard_canifa__ultra_descriptions SET size = ? WHERE id = ?",
(filtered_table, row_id)
)
updated += 1
if updated % 100 == 0:
print(f" Progress: {updated}/{total}")
conn.commit()
except json.JSONDecodeError as e:
errors += 1
print(f" ⚠️ JSON parse error on row {row_id}: {e}")
except Exception as e:
errors += 1
print(f" ⚠️ Error on row {row_id}: {e}")
conn.commit()
# Verify
cur.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ultra_descriptions WHERE size IS NOT NULL")
count = cur.fetchone()[0]
print(f"\n✅ Done! Updated {updated} rows. Total with size: {count}. Errors: {errors}")
# Show sample
cur.execute("""
SELECT id, size_scale, size
FROM pg__dashboard_canifa__ultra_descriptions
WHERE size IS NOT NULL
LIMIT 1
""")
sample = cur.fetchone()
if sample:
print("\n📄 Sample output:")
print(f" ID: {sample[0]}")
print(f" size_scale: {sample[1]}")
print(f" size (filtered):")
print(" " + sample[2].replace('\n', '\n '))
conn.close()
if __name__ == "__main__":
main()
This source diff could not be displayed because it is too large. You can view the blob instead.
-- 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', '{}');
-- ============================================================
-- AI Outfit Tables — dashboard_canifa schema
-- Author: Antigravity / Canifa AI Stylist Engine
-- Created: 2026-04-20
-- ============================================================
-- Purpose:
-- Lưu trực tiếp kết quả gợi ý outfit theo từng sản phẩm nguồn + dịp mặc.
-- Thay thế việc scan 1738 sản phẩm lúc runtime bằng 1 SELECT đơn giản.
--
-- Quan hệ:
-- ai_outfit_set (1 SP nguồn + 1 dịp)
-- └─ ai_outfit_items (N sản phẩm gợi ý, mỗi item giữ role + rank + score)
-- ============================================================
CREATE SCHEMA IF NOT EXISTS dashboard_canifa;
-- Bảng 1: Nhóm outfit theo (source_code, occasion_tag)
CREATE TABLE IF NOT EXISTS dashboard_canifa.ai_outfit_set (
id SERIAL PRIMARY KEY,
source_code VARCHAR(50) NOT NULL, -- SKU sản phẩm đang xem (magento_ref_code)
occasion_tag VARCHAR(50) NOT NULL, -- di_lam | di_choi | mac_nha | du_lich | the_thao
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (source_code, occasion_tag)
);
CREATE INDEX IF NOT EXISTS idx_outfit_set_source
ON dashboard_canifa.ai_outfit_set(source_code);
-- Bảng 2: Chi tiết từng sản phẩm trong outfit
CREATE TABLE IF NOT EXISTS dashboard_canifa.ai_outfit_items (
id SERIAL PRIMARY KEY,
outfit_id INT NOT NULL REFERENCES dashboard_canifa.ai_outfit_set(id) ON DELETE CASCADE,
target_code VARCHAR(50) NOT NULL, -- SKU sản phẩm được gợi ý
role VARCHAR(30) NOT NULL, -- top | bottom | outer | shoes | accessory | bag
rank SMALLINT NOT NULL DEFAULT 1, -- thứ tự ưu tiên trong cùng role (1=tốt nhất)
score SMALLINT DEFAULT 0, -- 0-100
reason TEXT DEFAULT '',
is_pinned BOOLEAN DEFAULT FALSE, -- stylist tay ghim cứng
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_outfit_items_outfit_id
ON dashboard_canifa.ai_outfit_items(outfit_id);
CREATE INDEX IF NOT EXISTS idx_outfit_items_target
ON dashboard_canifa.ai_outfit_items(target_code);
-- 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.
This source diff could not be displayed because it is too large. You can view the blob instead.
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 source diff could not be displayed because it is too large. You can view the blob instead.
-- 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);
import sqlite3
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def update_sqlite_colors():
db_path = "D:\\cnf\\chatbot-canifa-feedback\\backend\\database\\canifa_ai_dump.sqlite"
logger.info(f"Connecting to local SQLite: {db_path}")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
query = """
UPDATE sr__test_db__magento_product_dimension_with_text_embedding
SET description_data_full = json_set(
description_data_full,
'$.mau_sac',
(
SELECT GROUP_CONCAT(DISTINCT master_color)
FROM sr__test_db__magento_product_dimension_with_text_embedding sub
WHERE sub.magento_ref_code = sr__test_db__magento_product_dimension_with_text_embedding.magento_ref_code
AND sub.master_color IS NOT NULL
AND sub.master_color != ''
)
)
WHERE description_data_full IS NOT NULL
AND json_valid(description_data_full) = 1;
"""
logger.info("Executing UPDATE query with GROUP_CONCAT and json_valid check...")
try:
cursor.execute("BEGIN TRANSACTION")
cursor.execute(query)
affected_rows = cursor.rowcount
conn.commit()
logger.info(f"✅ Successfully updated {affected_rows} rows with concatenated colors!")
# Test 1 mã
cursor.execute("""
SELECT magento_ref_code, json_extract(description_data_full, '$.mau_sac')
FROM sr__test_db__magento_product_dimension_with_text_embedding
WHERE magento_ref_code = '1AK25C001' LIMIT 1
""")
sample = cursor.fetchone()
logger.info(f"🔍 Sample result for 1AK25C001: {sample}")
except Exception as e:
conn.rollback()
logger.error(f"❌ Error during update: {e}")
finally:
conn.close()
if __name__ == "__main__":
update_sqlite_colors()
import re
from collections import Counter
with open('test_db.magento_product_dimension_with_text_embedding.sql', encoding='utf-8', errors='ignore') as f:
data = f.read()
# Find the column order from INSERT statement
col_match = re.search(r'INSERT INTO[^(]+\(([^)]+)\)', data, re.DOTALL)
if col_match:
cols = [c.strip().strip('`') for c in col_match.group(1).split(',')]
season_idx = cols.index('season') if 'season' in cols else -1
print(f"Column order found. 'season' is at index: {season_idx}")
print(f"Columns around season: {cols[max(0,season_idx-2):season_idx+3]}")
else:
print("Could not find INSERT column list")
season_idx = -1
# Extract VALUES rows - find all value tuples
# Look for patterns like ('...', '...', NULL, ...)
rows = re.findall(r'\(([^;]+?)\)(?:,|\s*;)', data, re.DOTALL)
print(f"\nTotal value rows found: {len(rows)}")
if season_idx >= 0 and rows:
seasons = []
for row in rows[:50]: # sample first 50
# Split by comma but respect quoted strings
parts = re.split(r",(?=(?:[^']*'[^']*')*[^']*$)", row.strip())
if len(parts) > season_idx:
val = parts[season_idx].strip().strip("'")
seasons.append(val)
counts = Counter(seasons)
print(f"\nSeason values (sample from first 50 rows):")
print("-" * 40)
for s, c in sorted(counts.items(), key=lambda x: -x[1]):
print(f"{c:5d} | '{s}'")
This source diff could not be displayed because it is too large. You can view the blob instead.
# API Documentation & UI Feature Updates
## 1. Reset / Archive Chat Feature
The user has requested a function to reset the chat session while preserving the history (archiving it).
### Backend Implementation
- **File:** `api/conservation_route.py`
- **Method:** `POST /api/history/archive`
- **Logic:**
1. Identify user (via `get_user_identity`).
2. Call `reset_limit_service.check_limit()` (Max 5 times/day).
3. Call `manager.archive_history(identity_key)`.
4. Returns `new_key`, `success` status, and `remaining_resets`.
### Frontend Implementation
- **File:** `static/index.html`
- **UI:** A reset button (🔄) in the chat header.
- **Action:** Calls value API. Displays error if limit exceeded.
## 2. Conversation Manager Updates
- **Logic:** `get_chat_history` has been updated to filter messages by `CURRENT_DATE AT TIME ZONE 'Asia/Ho_Chi_Minh'`.
- **Archiving:** New `archive_history` method renames the `identity_key`.
## 3. Limits
- **Chat Context:** Only the last 15 messages are sent to the LLM (modified in `agent/controller.py`).
- **Reset Limit:** Authenticated users (and guests) are limited to 5 resets per day.
Oke bro, tao hiểu rõ ý bro rồi! Để tao tóm gọn lại toàn bộ kế hoạch bằng tiếng Việt cho bro nghe rõ ràng nhé.
Hiểu đúng ý của bro
Bro đang muốn chia làm hai giai đoạn rõ ràng. Giai đoạn một là sửa code chatbot, bỏ phần OpenTelemetry instrumentation hiện tại đi và chuyển sang dùng Langfuse SDK thuần túy để trace LLM calls. Giai đoạn hai là setup infrastructure monitoring hoàn toàn độc lập, dùng cAdvisor để thu thập container metrics từ Docker, kết hợp với Prometheus và Grafana để visualize.
GIAI ĐOẠN 1: Chuyển sang Langfuse SDK thuần túy
Bước đầu tiên là bro cần cleanup code hiện tại. Hiện tại trong server.py của bro đang có đống code setup OpenTelemetry với TracerProvider, OTLPSpanExporter, LoggingInstrumentor. Tất cả những thứ này sẽ được remove sạch. Bro sẽ giữ lại file server đơn giản, chỉ focus vào khởi động FastAPI app mà thôi.
Bước tiếp theo là enable Langfuse đầy đủ. Hiện tại trong code của bro, Langfuse đang bị comment out ở nhiều chỗ với note "TẮT TẠM - Tránh rate limit". Bro cần uncomment tất cả những đoạn này. Cụ thể trong file langfuse_client.py đã có sẵn hàm initialize_langfuse() và get_callback_handler() rồi, bro chỉ cần gọi nó khi app startup.
Trong controller.py, bro cần sửa hàm _prepare_execution_context() để attach Langfuse CallbackHandler vào RunnableConfig. Thay vì dùng OpenTelemetry span để track, bro sẽ dùng Langfuse callback để tự động capture tất cả LangChain runs, bao gồm LLM calls, tool calls, và toàn bộ conversation flow.
Điều quan trọng là bro cần set metadata cho Langfuse trace. Khi tạo CallbackHandler, bro nên truyền vào user_id, session_id, và các custom tags để sau này filter dễ dàng trong Langfuse dashboard. Ví dụ như trace_id có thể là conversation_id, user_id để group theo user, tags để đánh dấu production hay staging environment.
Cuối cùng là test để đảm bảo Langfuse đang hoạt động. Bro gửi vài requests test, sau đó vào Langfuse dashboard kiểm tra xem có traces xuất hiện không, có đủ thông tin về LLM model, tokens, latency không. Nếu thấy đủ data rồi thì giai đoạn một hoàn tất.
GIAI ĐOẠN 2: Setup Container Monitoring với cAdvisor
Giai đoạn này hoàn toàn độc lập với code, bro chỉ cần làm việc với Docker và configuration files.
Đầu tiên bro cần tạo file docker-compose.monitoring.yml riêng cho monitoring stack. File này sẽ define ba services: cAdvisor, Prometheus, và Grafana. Lý do tách riêng là để bro có thể bật tắt monitoring stack độc lập mà không ảnh hưởng đến chatbot service chính.
Service cAdvisor trong docker-compose sẽ mount nhiều thứ từ host vào container. Bro cần mount /var/run/docker.sock để cAdvisor đọc được Docker daemon, mount /sys để đọc cgroups metrics, mount /var/lib/docker để đọc thông tin containers. Expose port 8080 để Prometheus có thể scrape metrics. Quan trọng là cAdvisor container phải chạy với privileged mode hoặc ít nhất có quyền đọc được những đường dẫn system này.
Tiếp theo là config Prometheus. Bro tạo file prometheus.yml để define scrape configs. Trong đó bro add một job tên là cadvisor với target là cadvisor:8080, scrape interval khoảng 15 giây là hợp lý. Prometheus sẽ tự động pull metrics từ endpoint này theo chu kỳ đã set.
Service Prometheus trong docker-compose sẽ mount file prometheus.yml vào /etc/prometheus/prometheus.yml, và mount volume để persist data vào /prometheus. Expose port 9090 để bro có thể access Prometheus UI kiểm tra targets đang healthy không.
Cuối cùng là Grafana. Service Grafana đơn giản nhất, chỉ cần expose port 3000 và mount volume để lưu dashboards. Khi start lần đầu, bro login vào Grafana với admin/admin, sau đó add Prometheus làm datasource với URL là http://prometheus:9090. Tiếp theo là import dashboard, bro có thể dùng dashboard có sẵn từ Grafana community như dashboard ID 193 cho Docker monitoring, hoặc tự tạo dashboard custom theo nhu cầu.
Sau khi setup xong, bro chạy docker-compose -f docker-compose.monitoring.yml up -d để start monitoring stack. Kiểm tra cAdvisor tại localhost:8080 xem có metrics không, kiểm tra Prometheus tại localhost:9090 xem đã scrape được từ cAdvisor chưa, và cuối cùng vào Grafana tại localhost:3000 để xem dashboard có data không.
Lưu ý quan trọng
Điều bro cần nhớ là hai hệ thống này hoạt động song song và hoàn toàn độc lập. Langfuse track application metrics như LLM calls, tokens, conversations. cAdvisor track infrastructure metrics như CPU, RAM, network của containers. Chúng không giao nhau, không conflict, và bro sẽ có một cái nhìn toàn diện về cả application lẫn infrastructure.
Một điểm nữa là khi stress test, bro sẽ mở hai dashboard cùng lúc. Langfuse dashboard để xem throughput của LLM calls, average latency, token consumption, cost estimation. Grafana dashboard để xem container có bị overload không, memory có tăng đột biến không, CPU có spike không. Kết hợp hai nguồn data này bro sẽ biết chính xác bottleneck nằm ở đâu, là do LLM API slow hay do container thiếu resources.
Tóm lại
Giai đoạn một là refactor code để dùng Langfuse SDK thay OpenTelemetry, focus vào LLM observability. Giai đoạn hai là setup Docker monitoring stack với cAdvisor, Prometheus, Grafana, hoàn toàn không động vào code. Hai giai đoạn này theo thứ tự, làm xong một mới qua hai, và cuối cùng bro có một hệ thống observability hoàn chỉnh cho cả application và infrastructure.
Rõ chưa bro? Tao có thiếu sót chi tiết nào không?
\ No newline at end of file
.\.venv\Scripts\activate
uvicorn server:app --host 0.0.0.0 --port 5000 --reload
uvicorn server:app --host 0.0.0.0 --port 5000
docker restart chatbot-backend
docker restart chatbot-backend && docker logs -f chatbot-backend
docker logs -f chatbot-backend
docker restart canifa_backend
sudo docker compose -f docker-compose.prod.yml up -d --build
Get-NetTCPConnection -LocalPort 5000 | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
taskkill /F /IM python.exe
netstat -ano | findstr :5000 | ForEach-Object { $_.Split()[-1] } | Sort-Object -Unique | ForEach-Object { taskkill /F /PID $_ }
\ No newline at end of file
/* ═══════════════════════════════════════
CANIFA STYLIST PRO — Light Mode Edition
═══════════════════════════════════════ */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@400;600;700;800&display=swap');
:root {
/* Brand Colors */
--primary: #C8102E;
--primary-hover: #A30D25;
--primary-glow: rgba(200, 16, 46, 0.15);
/* Neutral Palette - Light Mode */
--bg-main: #F8F9FA;
--bg-surface: #FFFFFF;
--bg-card: #FFFFFF;
--bg-bubble-user: #C8102E;
--bg-bubble-ai: #F1F3F5;
--bg-input: #F8F9FA;
/* Accents */
--accent-blue: #007AFF;
--accent-gray: #E9ECEF;
/* Typography */
--text-main: #121212;
--text-secondary: #495057;
--text-muted: #ADB5BD;
--font-sans: 'Inter', sans-serif;
--font-display: 'Outfit', sans-serif;
/* Borders & Shadows */
--border: rgba(0, 0, 0, 0.08);
--border-bright: rgba(0, 0, 0, 0.12);
--shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
--shadow-md: 0 8px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 16px 48px rgba(0,0,0,0.12);
/* Transitions */
--ease: cubic-bezier(0.16, 1, 0.3, 1);
}
* { box-sizing: border-box; }
body {
font-family: var(--font-sans);
margin: 0; padding: 0;
background-color: var(--bg-main);
color: var(--text-main);
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
/* ═══ NAVIGATION ═══ */
.nav-header {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
padding: 12px 32px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
z-index: 1000;
}
.nav-header h1 {
margin: 0;
font-family: var(--font-display);
font-weight: 800;
font-size: 1.2rem;
color: var(--primary);
letter-spacing: 2px;
}
.nav-links { display: flex; gap: 12px; }
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
padding: 8px 16px;
border-radius: 10px;
background: var(--accent-gray);
transition: all 0.3s var(--ease);
font-size: 13px;
font-weight: 600;
}
.nav-links a:hover { background: #DEE2E6; transform: translateY(-2px); color: var(--text-main); }
.nav-links a.active { background: var(--primary); color: white; box-shadow: 0 4px 12px var(--primary-glow); }
/* ═══ MAIN LAYOUT ═══ */
.main-content {
height: calc(100vh - 64px);
max-width: 1600px;
margin: 0 auto;
padding: 24px;
display: flex;
gap: 24px;
}
/* CHAT CONTAINER */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-surface);
border-radius: 24px;
border: 1px solid var(--border);
box-shadow: var(--shadow-md);
overflow: hidden;
position: relative;
}
/* CONFIG BAR */
.config-bar {
padding: 16px 24px;
background: #F8F9FA;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.config-item { display: flex; align-items: center; gap: 8px; }
.config-item label { font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; }
.config-item input {
background: white;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-main);
padding: 6px 12px;
font-size: 12px;
outline: none;
}
.config-item input:focus { border-color: var(--primary); }
.btn-action {
padding: 8px 16px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
border: none;
cursor: pointer;
transition: all 0.3s var(--ease);
}
.btn-refresh { background: #007AFF; color: white; }
.btn-prompt { background: #FAB005; color: #000; }
.btn-clear { background: #FA5252; color: white; }
/* CHAT BOX */
.chat-box {
flex: 1;
overflow-y: auto;
padding: 32px;
display: flex;
flex-direction: column;
gap: 24px;
scroll-behavior: smooth;
background: #FFFFFF;
}
.msg-group { max-width: 85%; display: flex; flex-direction: column; }
.msg-group.user { align-self: flex-end; align-items: flex-end; }
.msg-group.bot { align-self: flex-start; align-items: flex-start; }
.sender-name { font-size: 11px; font-weight: 800; color: var(--text-muted); text-transform: uppercase; margin-bottom: 6px; letter-spacing: 1px; }
.bubble {
padding: 16px 20px;
border-radius: 20px;
font-size: 15px;
line-height: 1.6;
animation: fadeIn 0.4s var(--ease);
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.bubble.user { background: var(--primary); color: white; border-bottom-right-radius: 4px; box-shadow: 0 4px 15px var(--primary-glow); }
.bubble.bot { background: var(--bg-bubble-ai); border: 1px solid var(--border-bright); color: var(--text-main); border-bottom-left-radius: 4px; }
.bubble .timestamp { display: block; font-size: 10px; opacity: 0.5; margin-top: 8px; text-align: right; }
/* PRODUCT CARDS */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
margin-top: 20px;
width: 100%;
}
.p-card {
background: white;
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
transition: all 0.4s var(--ease);
display: flex;
flex-direction: column;
}
.p-card:hover { transform: translateY(-8px); border-color: var(--primary); box-shadow: var(--shadow-lg); }
.p-img-wrap { position: relative; height: 260px; overflow: hidden; background: #F8F9FA; }
.p-img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.8s var(--ease); }
.p-card:hover .p-img { transform: scale(1.05); }
.p-badge {
position: absolute; top: 12px; left: 12px; background: var(--primary); color: white;
font-size: 10px; font-weight: 800; padding: 4px 10px; border-radius: 6px; z-index: 10;
}
.p-content { padding: 16px; flex: 1; display: flex; flex-direction: column; }
.p-sku { font-size: 11px; font-weight: 700; color: var(--text-muted); margin-bottom: 4px; }
.p-name { font-size: 14px; font-weight: 600; color: var(--text-main); margin-bottom: 12px; height: 40px; overflow: hidden; }
.p-price-row { display: flex; align-items: center; gap: 8px; margin-top: auto; margin-bottom: 16px; }
.p-price-sale { font-size: 18px; font-weight: 800; color: var(--primary); }
.p-price-old { font-size: 13px; color: var(--text-muted); text-decoration: line-through; }
.p-btn {
background: #F8F9FA; color: var(--text-main); border: 1px solid var(--border);
padding: 10px; border-radius: 10px; text-decoration: none; font-size: 12px; font-weight: 700;
text-align: center; transition: all 0.3s;
}
.p-btn:hover { background: var(--primary); border-color: var(--primary); color: white; }
/* INPUT BAR */
.input-bar {
padding: 24px;
background: var(--bg-surface);
border-top: 1px solid var(--border);
}
.input-inner {
background: #F8F9FA;
border: 1px solid var(--border-bright);
border-radius: 20px;
padding: 8px 8px 8px 24px;
display: flex;
gap: 12px;
align-items: center;
}
.input-inner input { flex: 1; background: transparent; border: none; color: var(--text-main); font-size: 16px; outline: none; }
.btn-send {
width: 48px; height: 48px; border-radius: 14px; background: var(--primary); color: white;
border: none; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
/* PROMPT PANEL */
.prompt-panel {
width: 0; opacity: 0; visibility: hidden;
background: white; border-left: 1px solid var(--border);
display: flex; flex-direction: column; transition: all 0.5s var(--ease);
}
.prompt-panel.open { width: 500px; opacity: 1; visibility: visible; }
.prompt-inner { padding: 32px; display: flex; flex-direction: column; height: 100%; gap: 20px; }
.prompt-textarea {
flex: 1; background: #F8F9FA; color: #495057; border: 1px solid var(--border);
border-radius: 12px; padding: 20px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.6;
}
<!-- <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa Chatbot Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 0;
background-color: #1e1e1e;
color: #e0e0e0;
}
/* Navigation Header */
.nav-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.nav-header h1 {
margin: 0;
color: white;
font-size: 1.5em;
}
.nav-links {
display: flex;
gap: 15px;
}
.nav-links a {
color: white;
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.2);
transition: all 0.3s;
font-weight: 500;
}
.nav-links a:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.nav-links a.active {
background: rgba(255, 255, 255, 0.4);
}
.main-content {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.container {
background: #2d2d2d;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
height: 90vh;
border: 1px solid #444;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #444;
}
.header h2 {
margin: 0;
color: #fff;
}
.config-area {
display: flex;
gap: 10px;
align-items: center;
}
input[type="text"] {
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
background: #3d3d3d;
color: #fff;
}
button {
padding: 8px 16px;
background: #007acc;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.2s;
font-weight: 500;
}
button:hover {
opacity: 0.9;
}
button:disabled {
background: #555;
cursor: not-allowed;
}
.chat-box {
flex: 1;
overflow-y: auto;
padding: 20px;
border: 1px solid #444;
border-radius: 8px;
margin-bottom: 20px;
background: #252526;
display: flex;
flex-direction: column;
gap: 10px;
}
.message-container {
display: flex;
flex-direction: column;
max-width: 80%;
}
.message-container.user {
align-self: flex-end;
align-items: flex-end;
}
.message-container.bot {
align-self: flex-start;
align-items: flex-start;
}
.sender-name {
font-size: 0.8em;
margin-bottom: 4px;
color: #aaa;
margin-left: 4px;
margin-right: 4px;
}
.message {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
word-wrap: break-word;
position: relative;
}
.message.user {
background: #007acc;
color: white;
border-bottom-right-radius: 2px;
}
.message.bot {
background: #3e3e42;
color: #e0e0e0;
border-bottom-left-radius: 2px;
border: 1px solid #555;
}
.message.system {
background: #3d2d2d;
color: #ff6b6b;
align-self: center;
font-size: 0.9em;
max-width: 90%;
border: 1px solid #552b2b;
}
.message.rate-limit-error {
background: linear-gradient(135deg, #3d2d2d 0%, #2d2d3d 100%);
border: 1px solid #ff6b6b;
padding: 16px;
max-width: 350px;
}
.timestamp {
font-size: 0.7em;
opacity: 0.7;
margin-top: 6px;
display: block;
text-align: right;
}
.input-area {
display: flex;
gap: 10px;
}
.input-area input {
flex: 1;
padding: 12px;
border: 1px solid #555;
border-radius: 8px;
font-size: 16px;
background: #3d3d3d;
color: #fff;
}
.input-area input:focus {
outline: 2px solid #007acc;
border-color: transparent;
}
.load-more {
text-align: center;
margin-bottom: 10px;
}
.load-more button {
background: #3e3e42;
color: #ccc;
font-size: 0.85em;
width: 100%;
border: 1px dashed #555;
}
.load-more button:hover {
background: #4e4e52;
color: #fff;
}
.typing-indicator {
font-style: italic;
color: #888;
font-size: 0.9em;
margin-bottom: 10px;
display: none;
margin-left: 10px;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #2d2d2d;
}
::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* Product Cards Styling */
.product-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #555;
}
.product-card {
background: #3d3d3d;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
border: 1px solid #555;
display: flex;
flex-direction: column;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
border-color: #667eea;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
background: #2d2d2d;
}
.product-card-body {
padding: 12px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.product-sku {
font-size: 0.75em;
color: #667eea;
font-weight: bold;
margin-bottom: 5px;
}
.product-name {
font-size: 0.9em;
color: #fff;
margin-bottom: 10px;
line-height: 1.3;
flex-grow: 1;
}
.product-price {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.price-original {
font-size: 0.85em;
color: #888;
text-decoration: line-through;
}
.price-sale {
font-size: 1.1em;
color: #ff6b6b;
font-weight: bold;
}
.price-regular {
font-size: 1.1em;
color: #4caf50;
font-weight: bold;
}
.product-link {
display: block;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.85em;
transition: all 0.3s;
}
.product-link:hover {
opacity: 0.9;
transform: scale(1.02);
}
/* Response Time */
.response-time {
font-size: 0.75em;
color: #888;
margin-top: 8px;
font-style: italic;
}
/* Per-Message Toggle Button */
.message-view-toggle {
display: flex;
gap: 5px;
background: #3d3d3d;
border-radius: 6px;
padding: 4px;
border: 1px solid #555;
margin-top: 10px;
width: fit-content;
}
.message-view-toggle button {
padding: 6px 12px;
font-size: 0.8em;
background: transparent;
color: #aaa;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.message-view-toggle button.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message-view-toggle button:hover:not(.active) {
background: #4d4d4d;
color: #fff;
}
/* Raw JSON View */
.raw-json-view {
background: #1e1e1e;
border: 1px solid #555;
border-radius: 8px;
padding: 12px;
margin-top: 10px;
overflow-x: auto;
}
.raw-json-view pre {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #d4d4d4;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Filtered content view */
.filtered-content {
display: block;
}
.raw-content {
display: none;
}
/* --- Modern Layout & Animations --- */
.main-content {
max-width: 1400px;
/* Wider container */
margin: 0 auto;
padding: 20px;
height: calc(100vh - 80px);
/* Fill remaining height */
box-sizing: border-box;
}
.main-layout {
display: flex;
height: 100%;
gap: 0;
/* Gap handled by margin in panel for smooth transition */
position: relative;
}
/* Chat Container flex fix */
.container {
flex: 1;
display: flex;
flex-direction: column;
background: #2d2d2d;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid #444;
height: 100%;
padding: 0;
overflow: hidden;
transition: all 0.3s ease;
z-index: 10;
}
/* Internal padding for chat container */
.chat-internal-wrapper {
padding: 20px;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
/* PROMPT PANEL - Slide In Style */
.prompt-panel {
width: 0;
opacity: 0;
background: #1e1e1e;
/* Darker contrast */
border-left: 1px solid #444;
border-radius: 16px;
display: flex;
flex-direction: column;
padding: 0;
/* Padding handled internally to avoid width jump */
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
margin-left: 0;
box-shadow: -5px 0 20px rgba(0, 0, 0, 0.3);
white-space: nowrap;
/* Prevent content flicker during width change */
}
.prompt-panel.open {
width: 500px;
/* Wider editor */
opacity: 1;
margin-left: 20px;
padding: 20px;
}
.prompt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #333;
padding-bottom: 15px;
}
.prompt-header h3 {
font-size: 1.2em;
color: #4fc3f7;
/* Nice blue accent */
display: flex;
align-items: center;
gap: 10px;
}
.prompt-textarea {
flex: 1;
background: #111;
color: #dcdccc;
/* Soft code color */
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
resize: none;
margin-bottom: 15px;
white-space: pre-wrap;
/* Wrap code */
overflow-y: auto;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5);
}
.prompt-textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.panel-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid #333;
}
.status-text {
font-size: 0.8em;
color: #666;
font-style: italic;
}
/* Buttons Update */
.action-btn {
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9em;
border: none;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-reload {
background: #333;
color: #aaa;
}
.btn-reload:hover {
background: #444;
color: white;
}
.btn-save {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.btn-close-panel {
background: transparent;
border: none;
color: #666;
font-size: 1.5rem;
cursor: pointer;
transition: color 0.2s;
line-height: 1;
}
.btn-close-panel:hover {
color: #ff6b6b;
}
</style>
</head>
<body>
<!-- Navigation Header -->
<div class="nav-header">
<h1>🤖 Canifa AI System</h1>
<div class="nav-links">
<a href="/static/index.html" class="active">💬 Chatbot</a>
</div>
</div>
<div class="main-content">
<div class="main-layout">
<!-- Chat Container -->
<div class="container">
<div class="chat-internal-wrapper">
<div class="header">
<h2>🤖 Canifa AI Chat</h2>
<div class="config-area" style="flex-wrap: wrap; display: flex; align-items: center; gap: 10px;">
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Device ID:</label>
<input type="text" id="deviceId" placeholder="auto-generated" style="width: 150px;"
onblur="saveConfig()" onchange="saveConfig()">
</div>
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Status:</label>
<!-- Status Indicator logic could go here later -->
</div>
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Access Token:</label>
<input type="text" id="accessToken" placeholder="Token (optional)" style="width: 150px;"
onblur="saveConfig()" onchange="saveConfig()">
</div>
<!-- Action Buttons -->
<button onclick="loadHistory(true)" title="Load History">↻ History</button>
<button onclick="togglePromptEditor()"
style="background: #e6b800; color: #2d2d2d; font-weight: bold;">📝 Prompt</button>
<button onclick="clearUI()" style="background: #d32f2f;">✗ UI</button>
</div>
</div>
<div class="chat-box" id="chatBox">
<div class="load-more" id="loadMoreBtn" style="display: none;">
<button onclick="loadHistory(false)">Load Older Messages ⬆️</button>
</div>
<div id="messagesArea" style="display: flex; flex-direction: column; gap: 15px;"></div>
</div>
<div class="typing-indicator" id="typingIndicator">
<span style="font-style: normal;">🤖</span> AI is thinking...
</div>
<div class="input-area">
<input type="text" id="userInput" placeholder="Type your message..."
onkeypress="handleKeyPress(event)" autocomplete="off">
<button onclick="sendMessage()" id="sendBtn">➤ Send</button>
<button onclick="resetChat()" id="resetBtn" title="Reset Session"
style="background: #ffc107; color: #333; font-weight: bold; padding: 0 20px; margin-left: 10px; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center;">🔄
Reset</button>
</div>
</div>
</div>
<!-- Prompt Editor Panel -->
<div class="prompt-panel" id="promptPanel">
<div class="prompt-header">
<h3>📝 System Prompt</h3>
<button class="btn-close-panel" onclick="togglePromptEditor()">×</button>
</div>
<textarea id="systemPromptInput" class="prompt-textarea" placeholder="Loading prompt content..."
spellcheck="false"></textarea>
<div class="panel-footer">
<span class="status-text" id="promptStatus">Ready to edit</span>
<div style="display: flex; gap: 10px;">
<button class="action-btn btn-reload" onclick="loadSystemPrompt()">↻ Reset</button>
<button class="action-btn btn-save" onclick="saveSystemPrompt()">💾 Save & Apply</button>
</div>
</div>
</div>
</div>
<script>
let messageHistory = []; // Store messages for reference
let isPromptPanelOpen = false;
async function resetChat() {
if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return;
const deviceId = document.getElementById('deviceId').value;
if (!deviceId) return alert("Missing Device ID");
try {
const response = await fetch('/api/history/archive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'device_id': deviceId
},
body: JSON.stringify({})
});
const data = await response.json();
if (response.ok && data.status === 'success') {
document.getElementById('messagesArea').innerHTML = '';
const remaining = data.remaining_resets !== undefined ? ` (Còn ${data.remaining_resets} lượt)` : '';
alert('✅ ' + (data.message || 'Reset thành công!') + remaining);
} else {
const errorMsg = data.message || data.detail || 'Không thể reset';
const prefix = data.error_code === 'RESET_LIMIT_EXCEEDED' ? '⚠️ ' : '❌ Lỗi: ';
alert(prefix + errorMsg);
}
} catch (error) {
console.error('Reset error:', error);
alert('Có lỗi xảy ra khi reset.');
}
}
function togglePromptEditor() {
const panel = document.getElementById('promptPanel');
isPromptPanelOpen = !isPromptPanelOpen;
if (isPromptPanelOpen) {
panel.classList.add('open');
loadSystemPrompt();
} else {
panel.classList.remove('open');
}
}
async function loadSystemPrompt() {
const textarea = document.getElementById('systemPromptInput');
textarea.value = "Loading...";
textarea.disabled = true;
try {
const response = await fetch('/api/agent/system-prompt');
const data = await response.json();
if (data.status === 'success') {
textarea.value = data.content;
} else {
textarea.value = "Error loading prompt: " + data.message;
}
} catch (error) {
textarea.value = "Error connecting to server.";
console.error(error);
} finally {
textarea.disabled = false;
}
}
async function saveSystemPrompt() {
const content = document.getElementById('systemPromptInput').value;
const statusLabel = document.getElementById('promptStatus');
if (!content) return;
if (!confirm('Bạn có chắc muốn lưu Prompt mới? Bot sẽ bị reset graph để học prompt mới này.')) {
return;
}
statusLabel.innerText = "Saving...";
try {
const response = await fetch('/api/agent/system-prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content })
});
const data = await response.json();
if (data.status === 'success') {
statusLabel.innerText = "Saved!";
alert('✅ Đã lưu Prompt thành công!\nBot đã sẵn sàng với prompt mới.');
} else {
statusLabel.innerText = "Error!";
alert('❌ Lỗi: ' + data.detail);
}
} catch (error) {
statusLabel.innerText = "Connection Error";
alert('❌ Lỗi kết nối server');
console.error(error);
}
}
function toggleMessageView(messageId) {
const filteredContent = document.getElementById('filtered-' + messageId);
const rawContent = document.getElementById('raw-' + messageId);
const filteredBtn = document.getElementById('filtered-btn-' + messageId);
const rawBtn = document.getElementById('raw-btn-' + messageId);
if (filteredContent.style.display === 'none') {
// Switch to filtered
filteredContent.style.display = 'block';
rawContent.style.display = 'none';
filteredBtn.classList.add('active');
rawBtn.classList.remove('active');
} else {
// Switch to raw
filteredContent.style.display = 'none';
rawContent.style.display = 'block';
rawBtn.classList.add('active');
filteredBtn.classList.remove('active');
}
}
let currentCursor = null;
let isTyping = false;
async function loadHistory(isRefresh) {
const deviceId = document.getElementById('deviceId').value;
const accessToken = document.getElementById('accessToken').value.trim();
const messagesArea = document.getElementById('messagesArea');
const loadMoreBtn = document.getElementById('loadMoreBtn');
if (!deviceId) {
alert('Please enter a Device ID');
return;
}
if (isRefresh) {
messagesArea.innerHTML = '';
currentCursor = null;
}
// Gọi API với device_id trong URL, nhưng gửi kèm headers để middleware resolve đúng identity
const url = `/api/history/${deviceId}?limit=20${currentCursor ? `&before_id=${currentCursor}` : ''}`;
// Build headers for identity resolution (middleware sẽ dùng token để override nếu có)
const headers = {
'Content-Type': 'application/json',
'device_id': deviceId
};
if (accessToken) {
headers['Authorization'] = 'Bearer ' + accessToken;
}
try {
const response = await fetch(url, { headers: headers });
const data = await response.json();
const messages = data.data || data;
const cursor = data.next_cursor || null;
if (Array.isArray(messages) && messages.length > 0) {
currentCursor = cursor;
if (isRefresh) {
// Refresh: reverse để oldest ở trên, newest ở dưới
const batch = [...messages].reverse();
batch.forEach(msg => appendMessage(msg, 'bottom'));
setTimeout(() => {
const chatBox = document.getElementById('chatBox');
chatBox.scrollTop = chatBox.scrollHeight;
}, 100);
} else {
// Load more: messages từ API theo DESC (newest first của batch cũ)
const chatBox = document.getElementById('chatBox');
const oldHeight = chatBox.scrollHeight;
for (let i = 0; i < messages.length; i++) {
appendMessage(messages[i], 'top');
}
// Adjust scroll to keep view stable
chatBox.scrollTop = chatBox.scrollHeight - oldHeight;
}
loadMoreBtn.style.display = currentCursor ? 'block' : 'none';
} else {
if (isRefresh) {
messagesArea.innerHTML = '<div class="message system">No history found. Start chatting!</div>';
}
loadMoreBtn.style.display = 'none';
}
} catch (error) {
console.error('Error loading history:', error);
alert('Failed to load history');
}
}
function appendMessage(msg, position = 'bottom') {
const messagesArea = document.getElementById('messagesArea');
// Container wrapper for alignment
const container = document.createElement('div');
container.className = `message-container ${msg.is_human ? 'user' : 'bot'}`;
// Sender Name Label
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerText = msg.is_human ? 'You' : 'Canifa AI';
container.appendChild(sender);
// Message Bubble
const div = document.createElement('div');
div.className = `message ${msg.is_human ? 'user' : 'bot'}`;
// Generate unique message ID for toggle
const messageId = 'hist-' + (msg.id || Date.now() + Math.random());
if (msg.is_human) {
// User message: simple text
div.innerText = msg.message;
} else {
// Bot message: add Widget/Raw JSON toggle
// FILTERED CONTENT (default visible)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.innerText = msg.message;
div.appendChild(filteredDiv);
// RAW CONTENT (hidden by default)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({
id: msg.id,
message: msg.message,
product_ids: msg.product_ids || [],
timestamp: msg.timestamp,
is_human: msg.is_human
}, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
div.appendChild(rawDiv);
// Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
div.appendChild(toggleDiv);
}
// Timestamp inside bubble
const time = document.createElement('span');
time.className = 'timestamp';
time.innerText = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
div.appendChild(time);
container.appendChild(div);
if (position === 'top') {
messagesArea.insertBefore(container, messagesArea.firstChild);
} else {
messagesArea.appendChild(container);
}
}
async function sendMessage() {
const input = document.getElementById('userInput');
const deviceIdInput = document.getElementById('deviceId');
const accessTokenInput = document.getElementById('accessToken');
const deviceId = deviceIdInput.value.trim();
const accessToken = accessTokenInput.value.trim();
const text = input.value.trim();
const sendBtn = document.getElementById('sendBtn');
const typingIndicator = document.getElementById('typingIndicator');
const chatBox = document.getElementById('chatBox');
if (!deviceId) {
alert('Please enter a Device ID first!');
deviceIdInput.focus();
return;
}
if (!text) return;
// Disable input
input.disabled = true;
sendBtn.disabled = true;
typingIndicator.style.display = 'block';
// Add user message immediately
appendMessage({
message: text,
is_human: true,
timestamp: new Date().toISOString(),
id: 'pending'
});
input.value = '';
chatBox.scrollTop = chatBox.scrollHeight;
// Save config to localStorage
saveConfig();
// Track response time
const startTime = Date.now();
try {
// Build headers
const headers = {
'Content-Type': 'application/json',
'device_id': deviceId
};
// Add Authorization if access token provided
if (accessToken) {
headers['Authorization'] = 'Bearer ' + accessToken;
}
const response = await fetch('/api/agent/chat', {
method: 'POST',
headers: headers,
body: JSON.stringify({
user_query: text,
device_id: deviceId
})
});
// Handle API Errors (Rate Limit, System Error) with Widget/Raw View
if (response.status === 429 || response.status === 500) {
const errorData = await response.json();
const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
const messageId = 'msg-error-' + Date.now();
// Create bot message container
const messagesArea = document.getElementById('messagesArea');
const container = document.createElement('div');
container.className = 'message-container bot';
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerText = 'Canifa AI';
container.appendChild(sender);
const botMsgDiv = document.createElement('div');
botMsgDiv.className = 'message bot';
// 1. FILTERED CONTENT (Widget View)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.style.color = '#ff6b6b';
// Extract message
const errorMessage = errorData.message ||
errorData.detail?.message ||
'Có lỗi xảy ra!';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${errorData.error_code || 'ERROR'}</div>
<div>${errorMessage}</div>
${errorData.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Đăng nhập ngay để tiếp tục!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
// 2. RAW CONTENT (JSON View)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(errorData, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// 3. Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
// Response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
container.appendChild(botMsgDiv);
messagesArea.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight;
input.disabled = false;
sendBtn.disabled = false;
typingIndicator.style.display = 'none';
return;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail?.message || errorData.detail || 'Có lỗi xảy ra');
}
const data = await response.json();
const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
// Generate unique message ID
const messageId = 'msg-' + Date.now();
// Store message data
messageHistory.push({
type: 'bot',
data: data,
responseTime: responseTime,
timestamp: new Date().toISOString(),
id: messageId
});
// Create bot message placeholder
const messagesArea = document.getElementById('messagesArea');
const container = document.createElement('div');
container.className = 'message-container bot';
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerText = 'Canifa AI';
container.appendChild(sender);
const botMsgDiv = document.createElement('div');
botMsgDiv.className = 'message bot';
if (data.status === 'success') {
// FILTERED CONTENT (default visible)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
// Display AI text response
const textDiv = document.createElement('div');
textDiv.innerText = data.ai_response || 'No response';
filteredDiv.appendChild(textDiv);
// Render product cards if available
if (data.product_ids && data.product_ids.length > 0) {
const productsContainer = document.createElement('div');
productsContainer.className = 'product-cards-container';
data.product_ids.forEach(product => {
const card = document.createElement('div');
card.className = 'product-card';
// Product image
const img = document.createElement('img');
img.src = product.thumbnail_image_url || 'https://via.placeholder.com/200';
img.alt = product.name;
img.onerror = function () { this.src = 'https://via.placeholder.com/200?text=No+Image'; };
card.appendChild(img);
// Product body
const body = document.createElement('div');
body.className = 'product-card-body';
// SKU
const sku = document.createElement('div');
sku.className = 'product-sku';
sku.innerText = product.sku;
body.appendChild(sku);
// Name
const name = document.createElement('div');
name.className = 'product-name';
name.innerText = product.name;
body.appendChild(name);
// Price
const priceDiv = document.createElement('div');
priceDiv.className = 'product-price';
if (product.sale_price && product.sale_price < product.price) {
// Show original price with strikethrough
const originalPrice = document.createElement('span');
originalPrice.className = 'price-original';
originalPrice.innerText = product.price.toLocaleString('vi-VN') + 'đ';
priceDiv.appendChild(originalPrice);
// Show sale price
const salePrice = document.createElement('span');
salePrice.className = 'price-sale';
salePrice.innerText = product.sale_price.toLocaleString('vi-VN') + 'đ';
priceDiv.appendChild(salePrice);
} else {
// Show regular price
const regularPrice = document.createElement('span');
regularPrice.className = 'price-regular';
regularPrice.innerText = product.price.toLocaleString('vi-VN') + 'đ';
priceDiv.appendChild(regularPrice);
}
body.appendChild(priceDiv);
// Link button
const link = document.createElement('a');
link.className = 'product-link';
link.href = product.url;
link.target = '_blank';
link.innerText = '🛍️ Xem chi tiết';
body.appendChild(link);
card.appendChild(body);
productsContainer.appendChild(card);
});
filteredDiv.appendChild(productsContainer);
}
botMsgDiv.appendChild(filteredDiv);
// RAW CONTENT (hidden by default)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({
status: data.status,
ai_response: data.ai_response,
product_ids: data.product_ids,
limit_info: data.limit_info || null
}, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// Add toggle button
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
// Add response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
} else {
// ERROR CASE: Limit exceeded or other errors
// FILTERED CONTENT (error message - default visible)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.style.color = '#ff6b6b';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${data.error_code || 'ERROR'}</div>
<div>${data.message || 'Unknown error'}</div>
${data.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Vui lòng đăng nhập để tiếp tục sử dụng!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
// RAW CONTENT (hidden by default)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({
status: data.status,
error_code: data.error_code,
message: data.message,
require_login: data.require_login,
limit_info: data.limit_info || null
}, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
}
container.appendChild(botMsgDiv);
messagesArea.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight;
} catch (error) {
console.error('Error sending message:', error);
appendMessage({
message: `Error: ${error.message}`,
is_human: false,
timestamp: new Date().toISOString(),
id: 'error'
});
} finally {
input.disabled = false;
sendBtn.disabled = false;
typingIndicator.style.display = 'none';
input.focus();
chatBox.scrollTop = chatBox.scrollHeight;
}
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
function clearUI() {
document.getElementById('messagesArea').innerHTML = '';
}
// Apply token from login prompt in rate limit error
function applyLoginToken() {
const tokenInput = document.getElementById('loginTokenInput');
if (tokenInput && tokenInput.value.trim()) {
document.getElementById('accessToken').value = tokenInput.value.trim();
saveConfig();
alert('✅ Token đã được lưu! Bạn có thể tiếp tục chat.');
} else {
alert('Vui lòng nhập Access Token!');
}
}
// Save config to localStorage (called on input change/blur)
function saveConfig() {
const deviceId = document.getElementById('deviceId').value.trim();
const accessToken = document.getElementById('accessToken').value.trim();
if (deviceId) {
localStorage.setItem('canifa_device_id', deviceId);
}
if (accessToken) {
localStorage.setItem('canifa_access_token', accessToken);
} else {
localStorage.removeItem('canifa_access_token');
}
}
// Generate UUID for device_id
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// Load config from localStorage on page load
window.onload = function () {
// Load or generate Device ID
let savedDeviceId = localStorage.getItem('canifa_device_id');
if (!savedDeviceId) {
savedDeviceId = 'device-' + generateUUID().substring(0, 8);
localStorage.setItem('canifa_device_id', savedDeviceId);
}
document.getElementById('deviceId').value = savedDeviceId;
// Load Access Token (optional)
const savedAccessToken = localStorage.getItem('canifa_access_token');
if (savedAccessToken) {
document.getElementById('accessToken').value = savedAccessToken;
}
// Auto-load history
setTimeout(() => loadHistory(true), 50);
};
</script>
</div> <!-- Close main-content -->
</body>
</html> -->
\ No newline at end of file
Xanh da trời/ Blue
Xanh lá cây/ Green
Màu xanh Jeans
Xanh than/ Aqua
\ No newline at end of file
import asyncio
import os
import sys
# Ensure backend is in path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common.starrocks_connection import get_db_connection
async def test_like():
sr = get_db_connection()
sql = """
SELECT master_color,
LOWER(master_color) LIKE '%xanh lá%' as b_like,
LOWER(master_color) LIKE '%xanh lá cây%' as b_like_cay
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE master_color LIKE '%xanh lá%' OR master_color LIKE '%Xanh lá%'
LIMIT 10
"""
rows = await sr.execute_query_async(sql)
for r in rows:
print(f"Color: '{r['master_color']}', LIKE '%xanh lá%': {r['b_like']}, LIKE '%xanh lá cây%': {r['b_like_cay']}")
print("-- Testing missing query --")
sql2 = """
SELECT COUNT(*) as c
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE LOWER(master_color) LIKE '%xanh lá%' OR LOWER(product_color_name) LIKE '%xanh lá%'
"""
rows2 = await sr.execute_query_async(sql2)
print(f"Total matched for '%xanh lá%': {rows2[0]['c']}")
if __name__ == "__main__":
asyncio.run(test_like())
temp-skills @ c124b3b1
Subproject commit c124b3b1744d877e124c2d9e87c7dfabd9f57535
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