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

update

parent 6f846bee
# CLAUDE.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
\ No newline at end of file
{
"enabledMcpjsonServers": ["playwright"],
"enableAllProjectMcpServers": false
}
......@@ -24,7 +24,9 @@
"PowerShell(head -c 2000)",
"PowerShell(Invoke-RestMethod -Uri \"http://localhost:5000/api/product-desc/list?limit=3&status=has_desc\" -UseBasicParsing)",
"PowerShell(Invoke-RestMethod -Uri \"http://localhost:5000/api/product-desc/tags-batch-status\" -UseBasicParsing)",
"PowerShell($job = Start-Job -ScriptBlock { & .venv\\\\Scripts\\\\python.exe scripts\\\\generate_tags_batch.py --limit 1928 } ; Wait-Job $job -Timeout 2700 ; Receive-Job $job -Keep)"
"PowerShell($job = Start-Job -ScriptBlock { & .venv\\\\Scripts\\\\python.exe scripts\\\\generate_tags_batch.py --limit 1928 } ; Wait-Job $job -Timeout 2700 ; Receive-Job $job -Keep)",
"PowerShell(Get-Content \"d:\\\\cnf\\\\chatbot-canifa-feedback\\\\backend\\\\api\\\\main_router.py\" | Select-Object -Index \\(100..108\\) | Format-Hex)",
"PowerShell(git checkout \"d:\\\\cnf\\\\chatbot-canifa-feedback\\\\backend\\\\api\\\\main_router.py\")"
]
}
}
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@modelcontextprotocol/server-playwright"]
}
}
}
{"roles": [], "role_max_items": {}}
\ No newline at end of file
{
"roles": [],
"role_max_items": {},
"auto_train_rules": [
{
"rule_id": "RUL-1000",
"feedback_id": "FB-1000",
"rule_type": "prompt_update",
"rule": "- Khi troi nong (tren 30 do): tuyet doi khong goi y ao khoac day.\n- Chi goi y ao phong, chat lieu mong nhe, thoang mat.",
"reasoning": "User reports hot weather, but the AI suggested heavy clothing.",
"confidence": 0.85,
"deployed_at": "2026-05-08T16:39:55.848539"
}
]
}
\ No newline at end of file
# AI Event Tracking — Test Guide
## Quick Test
### 1. Start Server
```bash
cd D:\cnf\chatbot-canifa-feedback
python backend/server.py
```
Server sẽ chạy tại `http://localhost:8000`
### 2. Run Automated Test
```bash
python test_ai_event_api.py
```
### 3. Manual API Test với curl
**Publish event:**
```bash
curl -X POST "http://localhost:8000/api/ai-event/publish" \
-H "Content-Type: application/json" \
-d '{
"event_type": "chat:search_completed",
"channel": "in_app",
"user_id": "test_user",
"data": {
"query": "áo 30/4",
"products_found": 5,
"elapsed_ms": 150
}
}'
```
**Get logs:**
```bash
curl "http://localhost:8000/api/ai-event/logs?limit=10"
```
**Get stats:**
```bash
curl "http://localhost:8000/api/ai-event/stats"
```
**Clear logs:**
```bash
curl -X DELETE "http://localhost:8000/api/ai-event/logs"
```
### 4. Open Web UI
- Navigate to: `http://localhost:8000/static/ai_event/main.html`
- Or access via: `http://localhost:8000/home/` → find "AI Event Tracking" in menu (if added)
### 5. Test Event Context Boost
To test event 30/4 boosting:
1. Send query to chatbot API:
```bash
curl -X POST "http://localhost:8000/api/chatbot/chat" \
-H "Content-Type: application/json" \
-d '{
"user_query": "tìm áo 30/4",
"user_id": "test_123"
}'
```
2. Check logs for `[EVENT BOOST]` message in server console
3. Expected behavior:
- Event context detected: "Giải phóng miền Nam (30/4)"
- Tags boosted: `["Màu đỏ", "Đi chơi / dạo phố"]`
- Color boosted: `"Màu đỏ"`
- Product lines boosted: `["Áo phông", "Quần jean", "Váy liền"]`
### 6. Check Event Types
```bash
curl "http://localhost:8000/api/ai-event/types"
```
Should return list of event types from `common.notification.events.EventType.ALL`
---
## Features Implemented
✅ API Routes (`/api/ai-event/*`)
- `GET /logs` — Retrieve event logs
- `POST /publish` — Publish new event
- `GET /stats` — Get statistics
- `DELETE /logs` — Clear all logs
- `GET /types` — List event types
✅ Web UI (`/static/ai_event/`)
- Live Feed — Real-time event display
- Statistics — Charts (Chart.js) + summary cards
- Test Publisher — Manual event testing
- Search & Filter — By type & text
- Auto-refresh toggle
✅ Event Context Integration
- `event_context` passed to `lead_search_tool`
- `_apply_event_context()` method boosts search params
- Logged with `[EVENT BOOST]` prefix
---
## Troubleshooting
**Server not starting?**
- Check Python dependencies: `pip install -r backend/requirements.txt`
- Ensure Redis is running (if enabled in config)
**API returns 404?**
- Ensure `ai_event_router` is imported in `main_router.py`
- Check server logs for import errors
**Events not appearing?**
- Check browser console for JS errors
- Verify API is accessible: `curl http://localhost:8000/health`
- Check server logs for event publication
**Event boost not working?**
- Check that `event_context` is in tool call logs
- Verify `event_hard.py` has the event definition
- Look for `[EVENT BOOST]` in server logs
# 🚀 AI Event Boost — Test Guide
## Quick Start (3 bước)
### 1. Start Server
```bash
cd D:\cnf\chatbot-canifa-feedback
python backend/server.py
```
Chờ thấy: `Uvicorn running on http://0.0.0.0:8000`
### 2. Mở Chatbot Debug UI
```
http://localhost:8000/static/ai_event/chatbot.html
```
### 3. Run Automated Tests
```bash
python quick_test.py
```
---
## 📋 Test Cases
### ✅ Test 1: Event 30/4 (Giải phóng miền Nam)
**Query:** `tìm áo 30/4`
**Expected:**
- Event context: `mentioned_event.name = "Giải phóng miền Nam (30/4)"`
- Tags boost: `["Màu đỏ", "Đi chơi / dạo phố"]`
- Master color: `"Màu đỏ"`
- Product lines: `["Áo phông", "Quần jean", "Váy liền"]`
- Products trả về: Màu đỏ, áo phông, quần jean
**Check:**
- Debug Panel → Event Context card
- Server log: `[EVENT BOOST] Applied event 'Giải phóng miền Nam (30/4)'`
---
### ✅ Test 2: Event Valentine
**Query:** `váy Valentine` hoặc `áo Valentine`
**Expected:**
- Event: `Valentine (14/2)`
- Master color: `"Màu hồng"`
- Product lines: `["Váy liền", "Áo kiểu", "Chân váy"]`
---
### ✅ Test 3: Event Tết
**Query:** `áo Tết` hoặc `quần Tết` hoặc `mặc Tết`
**Expected:**
- Event: `Tết Nguyên Đán`
- Master color: `"Màu đỏ"`
- Product lines: `["Váy liền", "Chân váy", "Áo kiểu", "Blazer"]`
---
### ✅ Test 4: Regular Search (No Event)
**Query:** `quần jean nam` hoặc `áo polo`
**Expected:**
- `mentioned_event: null`
- Không có boost
- Search normal
---
### ✅ Test 5: Event Publish API
Test publish event vào event bus:
```bash
curl -X POST "http://localhost:8000/api/ai-event/publish" \
-H "Content-Type: application/json" \
-d '{
"event_type": "chat:search_completed",
"channel": "in_app",
"user_id": "test",
"data": {"query": "test"}
}'
```
Check logs:
```bash
curl "http://localhost:8000/api/ai-event/logs?limit=1"
```
---
## 🔍 Debug Panel Guide
Chatbot Debug UI có 4 card:
### 1. Event Context
- **Today**: Ngày hiện tại
- **Season**: Mùa (Xuân/Hè/Thu/Đông)
- **Mentioned**: Event được nhắc trong query
- **Next**: Event sắp tới
- **Details**: Full JSON của event context
### 2. Pipeline Flow
Hiển thị flow xử lý:
- 🔧 **Classifier**: Thời gian classify + log reasoning
- 📦 **Tool**: Thời gian search + số sản phẩm
- 💬 **Stylist**: Thời gian generate response
Click vào mỗi node để xem chi tiết (nếu có).
### 3. Products
Hiển thị danh sách sản phẩm trả về:
- Ảnh thumbnail
- Tên + SKU
- Giá (giá sale + discount)
- Click xem chi tiết
### 4. User Insight
- **Stage**: Giai đoạn mua hàng (1-5)
- **Tone**: Giọng điệu tư vấn
- **Goal**: Mục tiêu khách hàng
- **Target**: Đối tượng mua
- **Latest**: Sản phẩm quan tâm gần nhất
---
## 📊 API Endpoints
### Chatbot
```
POST /api/chatbot/chat
Content-Type: application/json
{
"user_query": "string",
"user_id": "string (optional)",
"history": [] // optional, array of {role, content}
}
Response:
{
"response": "string",
"elapsed_ms": number,
"lead_stage": {...},
"updated_insight": {...},
"pipeline": [...],
"product_ids": [...],
"products": [...],
"event_context": {...} // ← NEW!
}
```
### Event Tracking
```
GET /api/ai-event/logs?limit=50&event_type=...&user_id=...
POST /api/ai-event/publish
GET /api/ai-event/stats
DELETE /api/ai-event/logs
GET /api/ai-event/types
```
---
## 🐛 Troubleshooting
**Server not running?**
```bash
# Check if port 8000 is in use
netstat -ano | findstr :8000
# Start server
python backend/server.py
```
**404 on /api/chatbot/chat?**
- Check `backend/api/main_router.py` có include `chatbot_router`
- Restart server sau khi sửa code
**No event boost?**
- Check `event_hard.py` có event definition
- Query phải match keyword/date pattern
- Look for `[EVENT BOOST]` in server logs
**Products not showing?**
- Check database connection (StarRocks)
- Lead search tool phải trả về products
- Xem pipeline log是否有 tool step error
---
## 📁 Files Modified/Created
### Modified:
- `backend/agent/event_agent/product_search_engine.py` — Add event_context boost
- `backend/agent/event_agent/lead_search_tool.py` — Pass event_context
- `backend/agent/event_agent/graph.py` — Inject event_context into tool_args
- `backend/api/main_router.py` — Register ai_event_router
### Created:
- `backend/api/ai_event/ai_event_route.py` — Event API
- `backend/static/ai_event/main.html` — Event tracking UI
- `backend/static/ai_event/ai_event.css` — Styles
- `backend/static/ai_event/ai_event.js` — Frontend logic
- `backend/static/ai_event/chatbot.html` — Chatbot debug UI
- `quick_test.py` — Automated test script
- `TEST_AUTO_GUIDE.md` — This file
---
## 🎯 Success Criteria
✅ Event context detected from query (30/4, Tết, Valentine, etc.)
✅ Event boost applied to search params (tags, color, product_lines)
✅ Event log published and visible in UI
✅ Pipeline flow shows correct steps with timing
✅ Products returned match event theme (red for 30/4, pink for Valentine)
✅ No errors in server logs
---
**Happy testing! 🚀**
"""Lead Stage Agent — AI #1 phân tích giai đoạn mua hàng của khách."""
This diff is collapsed.
"""
Hardcoded event logic for Vietnam holidays and seasons.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from datetime import date, datetime
from typing import Optional
@dataclass(frozen=True)
class EventDef:
name: str
month: int
day: int
tags: list[str]
keywords: list[str]
master_color: Optional[str] = None
product_line_vn: Optional[list[str]] = None
_EVENTS: list[EventDef] = [
EventDef(
name="Ngày Giải phóng miền Nam (30/4)",
month=4,
day=30,
tags=["Màu đỏ", "Đi chơi / dạo phố"],
keywords=["cờ đỏ sao vàng", "lễ hội", "đi chơi"],
master_color="Màu đỏ",
product_line_vn=["Áo phông", "Quần jean", "Váy liền"],
),
EventDef(
name="Quốc khánh (2/9)",
month=9,
day=2,
tags=["Màu đỏ", "Đi chơi / dạo phố"],
keywords=["cờ đỏ sao vàng", "quốc khánh", "lễ hội"],
master_color="Màu đỏ",
product_line_vn=["Áo phông", "Quần jean", "Váy liền"],
),
EventDef(
name="Tết Nguyên Đán",
month=1,
day=1,
tags=["Màu đỏ", "Đi chơi / dạo phố"],
keywords=["Tết", "du xuân", "may mắn"],
master_color="Màu đỏ",
product_line_vn=["Váy liền", "Chân váy", "Áo kiểu", "Áo sơ mi", "Quần Khaki", "Áo dài"],
),
EventDef(
name="Ngày Quốc tế Phụ nữ (8/3)",
month=3,
day=8,
tags=["Đi chơi / dạo phố", "Đi tiệc"],
keywords=["thanh lịch", "duyên dáng", "nữ tính"],
product_line_vn=["Váy liền", "Chân váy", "Áo kiểu", "Blazer"],
),
EventDef(
name="Ngày Phụ nữ Việt Nam (20/10)",
month=10,
day=20,
tags=["Đi chơi / dạo phố", "Đi tiệc"],
keywords=["thanh lịch", "sang trọng", "duyên dáng"],
product_line_vn=["Váy liền", "Chân váy", "Áo kiểu", "Blazer"],
),
EventDef(
name="Valentine (14/2)",
month=2,
day=14,
tags=["Màu hồng", "Đi tiệc"],
keywords=["Valentine", "lãng mạn", "duyên dáng"],
master_color="Màu hồng",
product_line_vn=["Váy liền", "Áo kiểu", "Chân váy"],
),
EventDef(
name="Giáng sinh (25/12)",
month=12,
day=25,
tags=["Màu đỏ", "Đi tiệc"],
keywords=["giáng sinh", "nổi bật", "vui vẻ"],
master_color="Màu đỏ",
product_line_vn=["Váy liền", "Áo kiểu", "Blazer"],
),
]
_KEYWORD_EVENT_MAP: dict[str, str] = {
"30/4": "Ngày Giải phóng miền Nam (30/4)",
"2/9": "Quốc khánh (2/9)",
"quốc khánh": "Quốc khánh (2/9)",
"tet": "Tết Nguyên Đán",
"tết": "Tết Nguyên Đán",
"valentine": "Valentine (14/2)",
"giáng sinh": "Giáng sinh (25/12)",
"noel": "Giáng sinh (25/12)",
"20/10": "Ngày Phụ nữ Việt Nam (20/10)",
"8/3": "Ngày Quốc tế Phụ nữ (8/3)",
}
def _get_season(d: date) -> str:
month = d.month
if month in (12, 1, 2):
return "Mùa đông"
if month in (3, 4, 5):
return "Mùa xuân"
if month in (6, 7, 8):
return "Mùa hè"
return "Mùa thu"
def _find_event_by_name(name: str) -> Optional[EventDef]:
for ev in _EVENTS:
if ev.name == name:
return ev
return None
def _find_event_by_date(month: int, day: int) -> Optional[EventDef]:
for ev in _EVENTS:
if ev.month == month and ev.day == day:
return ev
return None
def _find_event_in_query(query: str) -> Optional[EventDef]:
if not query:
return None
query_lower = query.lower()
# Match explicit date like 30/4, 2-9, 2.9
date_match = re.search(r"\b(\d{1,2})[\./-](\d{1,2})\b", query_lower)
if date_match:
day = int(date_match.group(1))
month = int(date_match.group(2))
ev = _find_event_by_date(month, day)
if ev:
return ev
for key, name in _KEYWORD_EVENT_MAP.items():
if key in query_lower:
return _find_event_by_name(name)
return None
def _next_event(today: date) -> Optional[EventDef]:
for ev in sorted(_EVENTS, key=lambda x: (x.month, x.day)):
if (ev.month, ev.day) >= (today.month, today.day):
return ev
return _EVENTS[0] if _EVENTS else None
def get_event_context(query: str | None, today: date | None = None) -> dict:
now = today or datetime.now().date()
season = _get_season(now)
mentioned = _find_event_in_query(query or "")
current = _find_event_by_date(now.month, now.day)
upcoming = _next_event(now)
def _event_to_dict(ev: EventDef | None) -> Optional[dict]:
if not ev:
return None
return {
"name": ev.name,
"date": f"{ev.day:02d}/{ev.month:02d}",
"tags": ev.tags,
"keywords": ev.keywords,
"master_color": ev.master_color,
"product_line_vn": ev.product_line_vn or [],
}
return {
"today": now.strftime("%d/%m/%Y"),
"season": season,
"mentioned_event": _event_to_dict(mentioned),
"current_event": _event_to_dict(current),
"next_event": _event_to_dict(upcoming),
}
This diff is collapsed.
This diff is collapsed.
"""
Lead Search Tool - LangChain @tool cho AI goi.
Lớp này giờ chỉ đóng vai trò Wrapper mỏng (Thin Wrapper).
Toàn bộ logic phức tạp đã được đưa sang ProductSearchEngine.
"""
import json
import logging
from langchain_core.tools import tool
from .product_search_engine import InferredSearch, LeadSearchInput, LiteralSearch, ProductSearchEngine
logger = logging.getLogger(__name__)
engine = ProductSearchEngine()
@tool(args_schema=LeadSearchInput)
async def lead_search_tool(
literal: LiteralSearch,
inferred: InferredSearch,
magento_ref_code: str | None = None,
reasoning: str | None = None,
user_insight: dict | None = None, # Từ graph state
event_context: dict | None = None, # Từ graph state (event_hard.get_event_context)
) -> str:
"""
Tim kiem san pham CANIFA theo Dual-Lane Architecture.
"""
req = LeadSearchInput(
literal=literal,
inferred=inferred,
magento_ref_code=magento_ref_code,
reasoning=reasoning,
event_context=event_context, # Truyền vào model
)
try:
# Gọi engine — event_context đã có trong req
result_dict = await engine.search(req, reasoning=reasoning, user_insight=user_insight)
return json.dumps(result_dict, ensure_ascii=False, default=str)
except Exception as e:
logger.error("Lead search tool error: %s", e, exc_info=True)
return json.dumps({"status": "error", "message": str(e)})
"""
Product Line Mapping
Key = DB product_line_vn (chính xác)
Value = list các từ khách hàng hay dùng (synonym)
"""
# DB value → [các từ khách hàng hay gọi]
PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo Sơ mi": ["áo sơ mi", "áo công sở", "áo đi làm", "sơ mi", "sơmi", "áo sơmi"],
"Áo Polo": ["áo polo", "áo cổ bẻ", "polo"],
"Áo phông": ["áo phông", "áo thun", "áo thun ngắn tay", "áo cổ v", "áo cổ tym", "áo cộc tay"],
"Áo nỉ có mũ": ["áo nỉ có mũ", "áo hoodie", "hoodie"],
"Áo nỉ": ["áo nỉ", "áo sweater", "sweater"],
"Áo mặc nhà": ["áo mặc nhà", "áo ngủ", "áo ở nhà"],
"Áo lót": ["áo lót", "áo ngực", "áo quây", "áo lót nữ", "áo lót nam", "áo lót trẻ em"],
"Áo len gilet": ["áo len gilet", "áo gile len"],
"Áo len": ["áo len", "áo len dài tay"],
"Áo kiểu": ["áo kiểu", "áo điệu", "áo nữ tính"],
"Áo khoác sợi": ["áo khoác sợi"],
"Áo khoác nỉ không mũ": ["áo khoác nỉ không mũ", "áo khoác sweater"],
"Áo khoác nỉ có mũ": ["áo khoác nỉ có mũ", "áo khoác hoodie", "áo khoác nỉ"],
"Áo khoác lông vũ": ["áo khoác lông vũ", "áo phao lông vũ", "áo lông vũ"],
"Áo khoác gió": ["áo khoác gió", "áo gió", "áo khoác mỏng"],
"Áo khoác gilet chần bông": ["áo khoác gilet chần bông", "áo khoác gilet trần bông", "áo gilet chần bông", "áo gilet trần bông"],
"Áo khoác gilet": ["áo khoác gilet", "áo gile", "gile"],
"Áo khoác dạ": ["áo khoác dạ", "áo dạ"],
"Áo khoác dáng ngắn": ["áo khoác dáng ngắn", "áo khoác croptop"],
"Áo khoác chống nắng": ["áo khoác chống nắng", "áo chống nắng"],
"Áo khoác chần bông": ["áo khoác chần bông", "áo khoác trần bông", "áo chần bông", "áo trần bông", "áo phao"],
"Áo khoác": ["áo khoác", "áo ấm", "áo rét"],
"Áo giữ nhiệt": ["áo giữ nhiệt", "áo tản nhiệt", "áo heattech"],
"Áo bra active": ["áo bra active", "áo bra", "bra", "áo tập", "áo thể thao"],
"Áo Body": ["áo body", "áo croptop", "croptop", "baby tee", "áo lửng", "áo dáng ngắn", "áo ôm"],
"Áo ba lỗ": ["áo ba lỗ", "áo sát nách", "tanktop", "tank top", "áo dây", "áo 2 dây", "áo hai dây"],
"Váy liền": ["váy liền", "đầm", "váy công sở", "đầm công sở", "váy liền thân", "đầm suông", "váy dài"],
"Chân váy": ["chân váy", "váy maxi", "váy midi", "chân váy dài", "chân váy chữ a", "chân váy công sở", "váy ngắn"],
"Quần giả váy": ["quần giả váy", "quần váy", "skort"],
"Quần soóc": ["quần soóc", "quần đùi", "quần short", "quần lửng", "quần ngố", "short", "quần đùi nam", "quần đùi nữ"],
"Quần nỉ": ["quần nỉ", "quần jogger", "quần ống bo chun", "jogger", "quần thể thao"],
"Quần mặc nhà": ["quần mặc nhà", "quần ngủ", "quần đùi mặc nhà"],
"Quần lót đùi": ["quần lót đùi", "quần sịp đùi", "quần boxer", "boxer", "sịp đùi", "quần xì đùi"],
"Quần lót tam giác": ["quần lót tam giác", "quần sịp tam giác", "quần brief", "brief", "sịp tam giác", "quần xì tam giác"],
"Quần lót": ["quần lót", "quần chip", "quần sịp", "quần trong", "quần nhỏ", "quần xơ lít", "quần xì", "sịp", "chip", "đồ lót"],
"Quần leggings mặc nhà": ["quần leggings mặc nhà", "quần legging mặc nhà"],
"Quần leggings": ["quần leggings", "leggings", "quần legging", "legging", "quần thun ôm"],
"Quần Khaki": ["quần khaki", "quần âu", "quần vải", "quần tây", "quần công sở", "quần đi làm", "quần âu nam", "quần âu nữ", "quần kaki"],
"Quần jean": ["quần jean", "quần bò", "quần jeans", "denim", "jeans", "bò", "jean", "quần dzin"],
"Quần giữ nhiệt": ["quần giữ nhiệt", "quần heattech"],
"Quần dài": ["quần dài", "quần suông", "quần ống rộng", "quần ống suông", "quần lưng thun"],
"Quần culottes": ["quần culottes", "culottes", "quần lửng ống rộng"],
"Quần Body": ["quần body", "quần ôm"],
"Pyjama": ["pyjama", "pajama", "đồ pijama"],
"Mũ": ["mũ", "nón", "phụ kiện Canifa", "phụ kiện"],
"Khăn tắm": ["khăn tắm", "khăn to", "phụ kiện"],
"Khăn mặt": ["khăn mặt", "khăn nhỏ", "phụ kiện"],
"Khăn lau đầu": ["khăn lau đầu", "phụ kiện"],
"Khăn": ["khăn", "khăn len", "khăn quàng cổ", "phụ kiện"],
"Găng tay chống nắng": ["găng tay chống nắng", "găng tay", "bao tay"],
"Chăn cá nhân": ["chăn cá nhân", "chăn", "mền"],
"Cardigan": ["cardigan", "áo khoác len", "áo cardigan"],
"Bộ thể thao": ["bộ thể thao", "đồ tập", "đồ thể thao"],
"Bộ quần áo": ["bộ quần áo", "đồ bộ", "set đồ"],
"Bộ mặc nhà": ["bộ mặc nhà", "đồ ngủ", "đồ mặc nhà", "đồ ở nhà", "bộ lanh"],
"Blazer": ["blazer", "áo vest", "vest"],
"Tất": ["tất", "vớ", "bao chân", "vớ chân", "tất chân"],
"Quần tất": ["quần tất", "quần vớ", "tất quần"],
"Mũ thể thao": ["mũ thể thao", "mũ snapback", "mũ lưỡi trai", "cap"],
"Khẩu trang": ["khẩu trang", "mask", "mặt nạ vải"],
"Túi xách": ["túi xách", "túi"],
}
# ==============================================================================
# AUTO-GENERATE reverse lookup: synonym → DB value
# "áo thun" → "Áo phông", "quần bò" → "Quần jean", ...
# ==============================================================================
SYNONYM_TO_DB: dict[str, str] = {}
for db_value, synonyms in PRODUCT_LINE_MAP.items():
for syn in synonyms:
SYNONYM_TO_DB[syn.lower()] = db_value
# ==============================================================================
# RELATED LINES: hỏi "áo bra" → tìm cả "Áo bra active" + "Áo lót" và ngược lại
# ==============================================================================
RELATED_LINES: dict[str, list[str]] = {
"Áo bra active": ["Áo lót"],
"Áo lót": ["Áo bra active"],
# Quần lót (chung) → mở rộng tìm cả Quần lót đùi (Trunk) + Quần lót tam giác (Brief)
"Quần lót": ["Quần lót đùi", "Quần lót tam giác"],
"Quần lót đùi": ["Quần lót", "Quần lót tam giác"],
"Quần lót tam giác": ["Quần lót", "Quần lót đùi"],
}
def get_related_lines(product_line: str) -> list[str]:
"""VD: get_related_lines("Áo bra active") → ["Áo bra active", "Áo lót"]"""
return [product_line] + RELATED_LINES.get(product_line, [])
# Pre-sort synonyms by length DESC for longest-match-first
_SORTED_SYNONYMS = sorted(SYNONYM_TO_DB.keys(), key=len, reverse=True)
def resolve_product_name(raw_name: str) -> str:
"""
Resolve synonym trong product_name → tên DB thật.
Dùng longest-match-first để tránh match sai. Cập nhật thay thế ở bất kỳ vị trí nào trong chuỗi.
VD:
"tìm áo cổ bẻ khaki" → "tìm áo polo khaki"
"áo thun disney" → "áo phông disney"
"quần bò ống rộng" → "quần jean ống rộng"
"""
result = raw_name.lower().strip()
# Custom rule cho sơ mi cộc tay -> sơ mi ngắn tay
if "sơ mi" in result and "cộc tay" in result:
result = result.replace("cộc tay", "ngắn tay")
for synonym in _SORTED_SYNONYMS:
if synonym in result:
db_value = SYNONYM_TO_DB[synonym]
# Thay thế tất cả các lần xuất hiện của synonym bằng db_value (để thường)
result = result.replace(synonym, db_value.lower())
return result
def resolve_product_line(raw_value: str) -> list[str]:
"""
Lookup keyword → DB product_line_vn.
Hỗ trợ '/' separator (VD: "Quần/ Váy").
Không tìm thấy → giữ nguyên (prefix match ở SQL).
"""
parts = [p.strip() for p in raw_value.split("/") if p.strip()]
resolved = []
for part in parts:
mapped = SYNONYM_TO_DB.get(part.lower())
if mapped:
resolved.append(mapped)
else:
resolved.append(part)
return resolved
This diff is collapsed.
This diff is collapsed.
import logging
logger = logging.getLogger(__name__)
SIZE_MAPPING = {
"NU": {
"XS": "Cao 1m47-1m53, Nặng 38-43kg",
"S": "Cao 1m50-1m55, Nặng 41-46kg",
"M": "Cao 1m55-1m63, Nặng 47-52kg",
"L": "Cao 1m60-1m65, Nặng 53-58kg",
"XL": "Cao 1m62-1m66, Nặng 59-64kg",
},
"NAM": {
"S": "Cao 1m62-1m68, Nặng 57-62kg",
"M": "Cao 1m69-1m73, Nặng 63-67kg",
"L": "Cao 1m71-1m75, Nặng 68-72kg",
"XL": "Cao 1m73-1m77, Nặng 73-77kg",
"XXL": "Cao 1m75-1m79, Nặng 78-82kg",
},
"QUAN_NU": {
"26": "Vòng eo 65cm, Vòng mông 79-87cm",
"27": "Vòng eo 67.5cm, Vòng mông 81-89cm",
"28": "Vòng eo 70cm, Vòng mông 84-92cm",
"29": "Vòng eo 72.5cm, Vòng mông 86-94cm",
"30": "Vòng eo 75cm, Vòng mông 89-97cm",
},
"QUAN_NAM": {
"29": "Vòng eo 79.5cm, Vòng mông 96.5cm",
"30": "Vòng eo 82cm, Vòng mông 99cm",
"31": "Vòng eo 84.5cm, Vòng mông 101.5cm",
"32": "Vòng eo 87cm, Vòng mông 104cm",
"33": "Vòng eo 89cm, Vòng mông 106.5cm",
},
"TRE_EM": {
"90": "Dành cho bé 2Y, Cao 90cm, 10-13kg",
"92": "Dành cho bé 2Y, Cao 88-94cm, 10-13kg",
"98": "Dành cho bé 2-3Y, Cao 95-101cm, 13-15kg",
"100": "Dành cho bé 3-4Y, Cao 100cm, 14-17kg",
"104": "Dành cho bé 3-4Y, Cao 101-107cm, 15-18kg",
"110": "Dành cho bé 4-5Y, Cao 107-113cm, 18-23kg",
"116": "Dành cho bé 6Y, Cao 113-119cm, 22-25kg",
"120": "Dành cho bé 6-7Y, Cao 120cm, 24-29kg",
"122": "Dành cho bé 7Y, Cao 119-125cm, 25-28kg",
"128": "Dành cho bé 8Y, Cao 125-131cm, 28-32kg",
"130": "Dành cho bé 8Y, Cao 130cm, 29-33kg",
"134": "Dành cho bé 9Y, Cao 131-137cm, 32-36kg",
"140": "Dành cho bé 9-11Y, Cao 137-145cm, 33-39kg",
"150": "Dành cho bé 11-12Y, Cao 150cm, 39-45kg",
"152": "Dành cho bé 11-12Y, Cao 145-157cm, 39-46kg",
"160": "Dành cho bé 13-14Y, Cao 160cm, 45-52kg",
"164": "Dành cho bé 13-14Y, Cao 157-169cm, 46-55kg",
},
"UNISEX": {
"XXS": "Cao 1m55-1m63, Nặng 47-52kg",
"XS": "Cao 1m60-1m65, Nặng 53-58kg",
"S": "Cao 1m62-1m68, Nặng 57-62kg",
"M": "Cao 1m69-1m73, Nặng 63-67kg",
"L": "Cao 1m71-1m75, Nặng 68-72kg",
"XL": "Cao 1m73-1m77, Nặng 73-77kg",
"XXL": "Cao 1m75-1m79, Nặng 79-82kg",
}
}
def determine_table_key(gender: str, product_line: str) -> str:
"""Xác định bảng size phù hợp dựa trên giới tính và dòng sản phẩm."""
gender = (gender or "").lower().strip()
product_line = (product_line or "").lower().strip()
is_jeans_or_khaki = any(x in product_line for x in ["jean", "khaki", "kaki", "quần âu", "quần tây"])
is_bottom = "quần" in product_line
# 1. Trẻ em
if gender in ["boy", "girl", "kid", "bé trai", "bé gái", "be trai", "be gai", "trẻ em"]:
return "TRE_EM"
# 2. Unisex
if gender == "unisex":
return "UNISEX"
# 3. Quần size số (Jeans/Khaki/Âu) - Nếu không phải quần size chữ
if is_bottom and is_jeans_or_khaki:
if gender in ["women", "nu", "nữ", "female"]:
return "QUAN_NU"
elif gender in ["men", "nam", "male"]:
return "QUAN_NAM"
# 4. Áo / Quần chun (Dùng bảng chuẩn Nam / Nữ)
if gender in ["women", "nu", "nữ", "female"]:
return "NU"
elif gender in ["men", "nam", "male"]:
return "NAM"
return ""
def build_size_message(gender: str, product_line: str, sizes: list[str], description: str = "") -> str:
"""
Sinh ra message hướng dẫn chọn size chỉ chứa các size thực tế đang có (available_sizes).
Nếu mô tả sản phẩm có từ khóa form dáng (ôm, rộng) thì đính kèm mẹo.
"""
if not sizes:
return ""
table_key = determine_table_key(gender, product_line)
if not table_key:
return "" # Không xác định được bảng size
mapping = SIZE_MAPPING.get(table_key, {})
if not mapping:
return ""
# Tạo chuỗi thông tin cho các size khả dụng
size_lines = []
# Sort sizes logically if possible, but keep original order for now to avoid complexity with letters vs numbers
for s in sizes:
s_upper = str(s).strip().upper()
if s_upper in mapping:
size_lines.append(f"Size {s_upper} ({mapping[s_upper]})")
if not size_lines:
return "" # Các size đưa vào không có trong bảng (ví dụ freesize)
# Sinh phần mô tả đối tượng
obj_str = "Sản phẩm Nữ"
if table_key == "NAM": obj_str = "Sản phẩm Nam"
elif table_key == "TRE_EM": obj_str = "Sản phẩm Trẻ em"
elif table_key == "UNISEX": obj_str = "Sản phẩm Unisex (Phi giới tính)"
elif table_key == "QUAN_NU": obj_str = "Quần Nữ (Size số)"
elif table_key == "QUAN_NAM": obj_str = "Quần Nam (Size số)"
base_msg = f"{obj_str}. Các size ĐANG CÒN HÀNG: " + "; ".join(size_lines) + "."
# Tìm mẹo tư vấn (fit style hints) - Viết theo dạng mớm lời trực tiếp (Directive) cho AI
desc_lower = (description or "").lower()
tips = []
if any(x in desc_lower for x in ["dáng ôm", "slim fit", "slim-fit", "body", "ôm sát"]):
tips.append("🔥 Chỉ dẫn cho AI: Sản phẩm này là form ôm (slim-fit). Khi tư vấn size, hãy chèn thêm câu khuyên khách cực khéo: 'Dáng này thiết kế ôm body tôn dáng, nếu anh/chị thích mặc cử động thoải mái hoặc hay mặc lót bên trong thì mình nên nhích lên 1 size cho dễ chịu nhé ạ!'")
elif any(x in desc_lower for x in ["oversize", "thùng thình", "rộng rãi", "boxy"]):
tips.append("🔥 Chỉ dẫn cho AI: Sản phẩm này là form Oversize (rộng rãi). Hãy dặn dò khách thật tinh tế: 'Mẫu này hãng đã làm form rộng rãi sẵn rồi, anh/chị cứ dựa theo chiều cao cân nặng lấy ĐÚNG SIZE là lên dáng thụng cực chất, không cần phải cố tăng size đâu ạ. Còn nếu thích mặc gọn gàng thì mới lùi 1 size nhé!'")
elif any(x in desc_lower for x in ["regular", "vừa vặn"]):
tips.append("🔥 Chỉ dẫn cho AI: Sản phẩm có form Regular chuẩn. Hãy chốt size dứt khoát: 'Mẫu này form dáng vừa vặn cực chuẩn, anh/chị cứ chiếu theo đúng chiều cao cân nặng lấy chuẩn size là mặc vừa in, không cần đắn đo ạ!'")
if tips:
base_msg += "\n\n" + "\n".join(tips)
return base_msg
......@@ -32,7 +32,7 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo bra active": ["áo bra active", "áo bra", "bra", "áo tập", "áo thể thao"],
"Áo Body": ["áo body", "áo croptop", "croptop", "baby tee", "áo lửng", "áo dáng ngắn", "áo ôm"],
"Áo ba lỗ": ["áo ba lỗ", "áo sát nách", "tanktop", "tank top", "áo dây", "áo 2 dây", "áo hai dây"],
"Váy liền": ["váy liền", "đầm", "váy công sở", "đầm công sở", "váy liền thân", "đầm suông", "váy dài"],
"Váy liền": ["váy liền", "đầm", "váy công sở", "đầm công sở", "váy liền thân", "đầm suông", "váy dài", "váy body"],
"Chân váy": ["chân váy", "váy maxi", "váy midi", "chân váy dài", "chân váy chữ a", "chân váy công sở", "váy ngắn"],
"Quần giả váy": ["quần giả váy", "quần váy", "skort"],
"Quần soóc": ["quần soóc", "quần đùi", "quần short", "quần lửng", "quần ngố", "short", "quần đùi nam", "quần đùi nữ"],
......
# AI Event Tracking API
from .ai_event_route import router as ai_event_router
from .event_boost_route import router as event_boost_router
__all__ = ["router", "event_boost_router"]
"""
AI Event Tracking API — Track và display AI agent events trong chatbot.
"""
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from datetime import datetime
import json
import logging
from common.event_bus import event_bus
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/ai-event", tags=["AI Event Tracking"])
class EventLog(BaseModel):
"""Model cho event log storage."""
id: str
timestamp: str
event_type: str
channel: str
user_id: Optional[str] = None
data: Dict[str, Any]
metadata: Optional[Dict[str, Any]] = None
# In-memory store (production nên dùng Redis/DB)
_event_logs: List[EventLog] = []
_max_logs = 1000 # Giới hạn số logs trong memory
def _generate_id() -> str:
"""Generate unique ID cho event."""
import uuid
return str(uuid.uuid4())[:8]
@router.get("/logs")
async def get_event_logs(
limit: int = 50,
event_type: Optional[str] = None,
user_id: Optional[str] = None
) -> List[EventLog]:
"""
Lấy danh sách event logs.
Args:
limit: Số lượng logs trả về (default 50)
event_type: Filter theo event type
user_id: Filter theo user ID
"""
logs = _event_logs.copy()
if event_type:
logs = [log for log in logs if log.event_type == event_type]
if user_id:
logs = [log for log in logs if log.user_id == user_id]
return logs[-limit:][::-1] # Return latest first
@router.post("/publish")
async def publish_event(request: Request) -> Dict[str, Any]:
"""
Publish một event mới.
Body:
{
"event_type": "chat:search_completed",
"channel": "in_app",
"user_id": "user123",
"data": { ... },
"metadata": { ... }
}
"""
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON")
event_type = body.get("event_type")
channel = body.get("channel", "in_app")
user_id = body.get("user_id")
data = body.get("data", {})
metadata = body.get("metadata", {})
if not event_type:
raise HTTPException(status_code=400, detail="event_type is required")
# Create log entry
log = EventLog(
id=_generate_id(),
timestamp=datetime.now().isoformat(),
event_type=event_type,
channel=channel,
user_id=user_id,
data=data,
metadata=metadata
)
# Store in memory
_event_logs.append(log)
if len(_event_logs) > _max_logs:
_event_logs.pop(0)
# Also publish to event bus nếu cần
try:
await event_bus.publish(event_type, {
"event_id": log.id,
"timestamp": log.timestamp,
"user_id": user_id,
**data
})
except Exception as e:
logger.warning(f"Failed to publish to event bus: {e}")
logger.info(f"[EVENT] {event_type} | user={user_id} | data={data}")
return {
"status": "ok",
"event_id": log.id,
"timestamp": log.timestamp
}
@router.get("/stats")
async def get_event_stats() -> Dict[str, Any]:
"""Get statistics về events."""
total = len(_event_logs)
# Count by type
by_type: Dict[str, int] = {}
for log in _event_logs:
by_type[log.event_type] = by_type.get(log.event_type, 0) + 1
# Count by channel
by_channel: Dict[str, int] = {}
for log in _event_logs:
by_channel[log.channel] = by_channel.get(log.channel, 0) + 1
# Recent activity (last hour)
one_hour_ago = datetime.now().timestamp() - 3600
recent = 0
for log in _event_logs:
try:
ts = datetime.fromisoformat(log.timestamp).timestamp()
if ts > one_hour_ago:
recent += 1
except:
pass
return {
"total": total,
"recent_1h": recent,
"by_type": by_type,
"by_channel": by_channel,
"max_capacity": _max_logs
}
@router.delete("/logs")
async def clear_event_logs() -> Dict[str, Any]:
"""Clear tất cả event logs."""
global _event_logs
count = len(_event_logs)
_event_logs = []
logger.info(f"Cleared {count} event logs")
return {
"status": "ok",
"cleared": count
}
@router.get("/types")
async def get_event_types() -> List[str]:
"""Lấy danh sách tất cả event types có sẵn."""
from common.notification.events import EventType
return EventType.ALL
"""
Event Boost Test API — Test event context boost trực tiếp.
"""
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from typing import Optional, Dict, Any
import logging
from agent.event_agent.event_hard import get_event_context
from agent.event_agent.product_search_engine import ProductSearchEngine, LeadSearchInput, LiteralSearch, InferredSearch
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/event-test", tags=["Event Boost Test"])
engine = ProductSearchEngine()
class EventBoostTestRequest(BaseModel):
"""Request để test event boost."""
query: str
product_line_vn: list[str] = []
gender_by_product: Optional[str] = None
master_color: Optional[str] = None
tags: list[str] = []
keywords: list[str] = []
price_min: Optional[int] = None
price_max: Optional[int] = None
size: Optional[str] = None
class EventBoostTestResponse(BaseModel):
"""Response từ event boost test."""
query: str
event_context: Dict[str, Any]
boosted_params: Dict[str, Any]
search_result: Dict[str, Any]
boost_applied: bool
message: str
@router.post("/boost", response_model=EventBoostTestResponse)
async def test_event_boost(request: EventBoostTestRequest):
"""
Test event boost — Cho query, xem event context và params được boost như thế nào.
Example:
```json
{
"query": "tìm áo 30/4",
"product_line_vn": ["Áo phông"],
"tags": [],
"keywords": []
}
```
"""
try:
# 1. Get event context từ query
event_context = get_event_context(request.query)
mentioned = event_context.get("mentioned_event")
# 2. Build inferred search với params ban đầu
inferred = InferredSearch(
product_line_vn=request.product_line_vn,
gender_by_product=request.gender_by_product,
master_color=request.master_color,
tags=request.tags,
keywords=request.keywords,
price_min=request.price_min,
price_max=request.price_max,
size=request.size,
)
# 3. Build lead search input
literal = LiteralSearch(raw_text=request.query)
lead_input = LeadSearchInput(
literal=literal,
inferred=inferred,
event_context=event_context # Truyền event context
)
# 4. Apply event boost (internal method)
engine._apply_event_context(lead_input, event_context)
# 5. Get boosted params
boosted = lead_input.inferred
boost_applied = bool(mentioned)
message = f"Event '{mentioned.get('name')}' boost applied" if boost_applied else "No event detected"
# 6. Optional: Run actual search (comment out nếu chỉ muốn test boost)
# search_result = await engine.search(lead_input)
return EventBoostTestResponse(
query=request.query,
event_context=event_context,
boosted_params={
"product_line_vn": boosted.product_line_vn,
"master_color": boosted.master_color,
"tags": boosted.tags,
"keywords": boosted.keywords,
},
search_result={"status": "ok", "note": "Search not executed in test mode"},
boost_applied=boost_applied,
message=message
)
except Exception as e:
logger.error(f"Event boost test error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/events")
async def list_events():
"""
List tất cả events có sẵn trong event_hard.py
"""
from agent.event_agent.event_hard import _EVENTS
events = []
for ev in _EVENTS:
events.append({
"name": ev.name,
"date": f"{ev.day:02d}/{ev.month:02d}",
"tags": ev.tags,
"keywords": ev.keywords,
"master_color": ev.master_color,
"product_line_vn": ev.product_line_vn,
})
return {
"total": len(events),
"events": events
}
@router.get("/test-sample")
async def test_sample_queries():
"""
Run sample test queries và trả về kết quả.
"""
test_queries = [
"tìm áo 30/4",
"váy Valentine",
"áo Tết",
"quần jean nam",
]
results = []
for query in test_queries:
try:
req = EventBoostTestRequest(query=query)
resp = await test_event_boost(req)
results.append({
"query": query,
"event_detected": resp.boost_applied,
"event_name": resp.event_context.get("mentioned_event", {}).get("name") if resp.boost_applied else None,
"boosted_params": resp.boosted_params,
})
except Exception as e:
results.append({
"query": query,
"error": str(e)
})
return {
"total": len(results),
"results": results
}
# Auto-Train API
from .autotrain_route import router
__all__ = ["router"]
This diff is collapsed.
......@@ -6,6 +6,7 @@ from api.api_sql.sql_chat_route import router as sql_chat_router
# SQL & Insights
from api.api_sql.text_to_sql_route import router as text_to_sql_router
from api.api_sql.user_insight_route import router as user_insight_router
from api.autotrain.autotrain_route import router as autotrain_router
from api.cache.cache_route import router as cache_router
from api.common.auth_route import router as auth_router
# Core AI & Chatbot
......@@ -104,6 +105,7 @@ api_router.include_router(text_to_sql_router)
api_router.include_router(sql_chat_router)
api_router.include_router(ai_sql_trace_router)
api_router.include_router(user_insight_router)
api_router.include_router(autotrain_router)
# Dashboards & Monitoring
api_router.include_router(dashboard_router)
......
This diff is collapsed.
This diff is collapsed.
chatbot_canifa @ f9b10ebe
Subproject commit f9b10ebef38a7af9ffb488678084f0ebac581afc
import sqlite3
import sys
sys.stdout.reconfigure(encoding='utf-8')
db_path = r"d:\cnf\chatbot-canifa-feedback\backend\database\canifa_ai_dump.sqlite"
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""
SELECT anchor_category, match_role, target_category, ai_reason
FROM pg__dashboard_canifa__chatbot_fashion_rules
WHERE anchor_category = 'Quần soóc' AND match_role = 'underwear'
LIMIT 10
""")
rows = cur.fetchall()
print("Underwear rules for Quần soóc:", len(rows))
for r in rows:
print(r)
cur.execute("""
SELECT match_role, COUNT(*) as cnt
FROM pg__dashboard_canifa__chatbot_fashion_rules
WHERE anchor_category = 'Quần soóc'
GROUP BY match_role
""")
print("\nAll roles for Quần soóc:")
for r in cur.fetchall():
print(r)
conn.close()
# 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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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