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

update

parent a9df6eb7
......@@ -19,7 +19,12 @@
"Bash(set POSTGRES_URL=postgresql://local:local@localhost:5432/local_app)",
"Bash(..\\\\\\\\venv\\\\\\\\Scripts\\\\\\\\python.exe -m uvicorn main:app --reload --host 0.0.0.0 --port 8000)",
"Bash(pkill -f \"uvicorn\")",
"Bash(taskkill //F //IM python.exe)"
"Bash(taskkill //F //IM python.exe)",
"PowerShell(curl -s \"http://localhost:5000/api/product-desc/list?limit=5\")",
"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)"
]
}
}
{
"disableYoloMode": false,
"mcpServers": {
"canifa-api": {
"url": "http://localhost:5000/mcp"
......
......@@ -23,6 +23,7 @@ from api.product.canifa_product_api import router as canifa_product_router
from api.product_desc.product_desc_route import router as product_desc_router
from api.product_desc.n8n_desc import router as n8n_desc_router
from api.product_desc.bulk_ops_route import router as bulk_ops_router
from api.product_desc.tags_direct_route import router as tags_direct_router
from api.fashion_matches.router import router as fashion_matches_router
from api.fashion_matches.simulator import router as fashion_matches_simulator_router
from api.store_search.ai_store_search import router as ai_store_search_router
......@@ -91,6 +92,7 @@ api_router.include_router(canifa_product_router)
api_router.include_router(product_desc_router)
api_router.include_router(n8n_desc_router)
api_router.include_router(bulk_ops_router)
api_router.include_router(tags_direct_router)
api_router.include_router(fashion_matches_router)
api_router.include_router(fashion_matches_simulator_router)
api_router.include_router(ai_store_search_router)
......
......@@ -945,12 +945,19 @@ async def batch_generate_tags():
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
# Find up to 500 products that either don't have the tags column populated or it's empty
cur.execute(f"SELECT internal_ref_code FROM {PG_TABLE} WHERE tags IS NULL LIMIT 500")
# Get up to 500 products that have clean_description (regardless of tags status)
# This will merge new tags with existing ones
cur.execute(f"""
SELECT internal_ref_code
FROM {PG_TABLE}
WHERE clean_description IS NOT NULL
AND BTRIM(clean_description) != ''
LIMIT 500
""")
rows = cur.fetchall()
if not rows:
return {"status": "success", "message": "Tất cả sản phẩm đã được gắn tag rồi, không cần chạy nữa!"}
return {"status": "success", "message": "Không tìm thấy sản phẩm nào có clean_description để gắn tag!"}
codes = [r[0] for r in rows]
......@@ -973,7 +980,7 @@ async def batch_generate_tags():
"current_code": codes[0] if codes else ""
})
return {"status": "success", "message": f"Đã đẩy {len(codes)} sản phẩm vào hàng đợi Auto-Tagging"}
return {"status": "success", "message": f"Đã đẩy {len(codes)} sản phẩm vào hàng đợi để merge tags mới (occasion/season/style/fit) với tags cũ (color palette)"}
except Exception as e:
logger.error(f"Error checking untagged products: {e}", exc_info=True)
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
......@@ -1010,12 +1017,13 @@ async def tags_batch_status():
class SingleProductTagsRequest(BaseModel):
internal_ref_code: str
internal_ref_code: str | None = None
magento_ref_code: str | None = None # FE sends this field
@router.post("/generate-tags-single", summary="Generate tags for a single product via Redis Queue")
async def generate_tags_single(req: SingleProductTagsRequest):
code = req.internal_ref_code
code = req.magento_ref_code or req.internal_ref_code or ""
if not code:
return JSONResponse(status_code=400, content={"status": "error", "message": "Missing internal_ref_code"})
......
......@@ -28,6 +28,28 @@ router = APIRouter(prefix="/api/product-desc", tags=["Ultra Description"])
TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding"
# ═══ Groq Config — Multi-Key Round-Robin ═══
GROQ_API_KEYS = [
"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",
"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",
"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",
"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",
"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",
"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",
]
_groq_key_index = 0
def _next_groq_key() -> str:
"""Get next Groq API key using round-robin rotation."""
global _groq_key_index
key = GROQ_API_KEYS[_groq_key_index % len(GROQ_API_KEYS)]
_groq_key_index += 1
return key
GROQ_API_KEY = GROQ_API_KEYS[0] # Backwards compat for imports
VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
ENRICH_MODEL = "openai/gpt-oss-120b"
# ═══ Groq Config — Multi-Key Round-Robin ═══
GROQ_API_KEYS = [
"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", # Key 1
......@@ -505,6 +527,9 @@ async def product_desc_list(
params=tuple(params + [limit, offset]),
)
# Ensure all products are mutable dicts (convert from MockRow if needed)
products = [dict(p) if not isinstance(p, dict) else p for p in products]
# Enrich with desc status from Postgres
for p in products:
magento_code = p.get("magento_ref_code") or p.get("internal_ref_code")
......@@ -1259,11 +1284,34 @@ async def delete_field(field_key: str):
# ═══ 11. BATCH GENERATE TAGS ═══
# Tags Allowed (Plain, No Prefix) — merge với color palette cũ
TAGS_ALLOWED = [
# Color tones
"trung tính", "sáng", "đậm",
# Specific colors
"đen", "trắng", "xám", "nâu", "vàng", "hồng", "xanh da trời", "xanh than", "tím", "đỏ", "cam", "xanh lá",
# Seasons
"mùa hè", "mùa đông", "mùa xuân", "mùa thu",
# Occasions
"đi học", "đi chơi", "đi tiệc", "dạo phố", "mặc nhà", "đi ngủ",
"thể thao", "đi dã ngoại", "đi biển", "đi làm", "giữ ấm", "thoáng mát"
"thể thao", "đi dã ngoại", "đi biển", "đi làm", "giữ ấm", "thoáng mát",
# Styles
"thanh lịch", "năng động", "basic", "cá tính", "dễ thương", "trẻ trung", "tối giản", "smart casual",
# Fit
"oversize", "slim", "regular", "wide leg", "cropped", "relaxed"
]
TAGS_PROMPT = f"""Bạn là chuyên gia phân loại sản phẩm. Hãy đọc phần mô tả sản phẩm và trích xuất đúng các tags phù hợp nhất.
HỆ THỐNG TAGS HỢP LỆ (Chỉ được chọn từ danh sách này):
{json.dumps(TAGS_ALLOWED, ensure_ascii=False)}
QUY TẮC:
- Trả về 1 mảng JSON chứa tối đa 6 tags.
- KHÔNG sáng tạo thêm tags mới ngoài danh sách.
- Ví dụ trả về: ["đi làm", "thoáng mát", "mùa hè"]
"""
async def _process_batch_tags(job_id: str, codes: list[str]):
"""Background task to generate tags for products."""
redis = redis_cache.get_client()
......@@ -1284,16 +1332,6 @@ async def _process_batch_tags(job_id: str, codes: list[str]):
done = 0
errors = 0
TAGS_PROMPT = f"""Bạn là chuyên gia phân loại sản phẩm. Hãy đọc phần mô tả sản phẩm và trích xuất đúng các tags phù hợp nhất.
HỆ THỐNG TAGS BẮT BUỘC (Chỉ được chọn từ danh sách này):
{json.dumps(TAGS_ALLOWED, ensure_ascii=False)}
QUY TẮC:
- Trả về 1 mảng JSON chứa tối đa 5 tags.
- KHÔNG sáng tạo thêm tags mới ngoài danh sách.
- Ví dụ trả về: ["đi làm", "thoáng mát"]
"""
for code in codes:
await redis.hset(progress_key, "current_code", code)
......@@ -1302,15 +1340,24 @@ QUY TẮC:
errors += 1
continue
# Get old tags (to merge)
old_tags = []
old_tags_raw = row.get("tags")
if old_tags_raw:
if isinstance(old_tags_raw, str):
try: old_tags = json.loads(old_tags_raw)
except: old_tags = []
elif isinstance(old_tags_raw, list):
old_tags = old_tags_raw
if not isinstance(old_tags, list):
old_tags = []
text = row.get("clean_description", "")
if not text:
# Try to get from raw description_data if clean is empty
desc_data = row.get("description_data", {})
if isinstance(desc_data, str):
try:
desc_data = json.loads(desc_data)
except:
desc_data = {}
try: desc_data = json.loads(desc_data)
except: desc_data = {}
if "mo_ta_chinh" in desc_data:
text = desc_data.get("mo_ta_chinh", "") + " " + desc_data.get("dip_mac", "")
......@@ -1330,8 +1377,14 @@ QUY TẮC:
if isinstance(parsed, list):
# Filter to only allowed tags
final_tags = [t for t in parsed if t in TAGS_ALLOWED]
UltraDescriptionDB.update_tags(code, final_tags)
new_tags = [t for t in parsed if t in TAGS_ALLOWED]
# Merge: keep old tags, add new ones not already present
merged_tags = old_tags[:]
for t in new_tags:
if t not in merged_tags:
merged_tags.append(t)
UltraDescriptionDB.update_tags(code, merged_tags)
done += 1
else:
errors += 1
......@@ -1345,31 +1398,36 @@ QUY TẮC:
await redis.hset(progress_key, "is_running", "false")
@router.post("/batch-generate-tags", summary="Generate semantic tags for all missing products")
@router.post("/batch-generate-tags", summary="Generate semantic tags for all products (merge with existing)")
async def batch_generate_tags_endpoint(background_tasks: BackgroundTasks):
"""Trigger background job to analyze and extract tags for products missing them."""
"""Trigger background job to add/merge tags for all products with clean_description."""
try:
from common.pool_wrapper import get_pooled_connection_compat
conn = get_pooled_connection_compat()
cur = conn.cursor()
# Get codes that have clean_descriptions but missing tags (tags='[]' or tags is null)
cur.execute(f"SELECT internal_ref_code FROM dashboard_canifa.ultra_descriptions WHERE clean_description IS NOT NULL AND BTRIM(clean_description) != '' AND (tags IS NULL OR tags = '[]'::jsonb)")
missing_rows = cur.fetchall()
# Get all products that have clean_description (regardless of current tags)
cur.execute(f"""
SELECT internal_ref_code
FROM dashboard_canifa.ultra_descriptions
WHERE clean_description IS NOT NULL
AND clean_description != ''
""")
all_rows = cur.fetchall()
cur.close()
conn.close()
codes = [r[0] for r in missing_rows]
codes = [r[0] for r in all_rows]
if not codes:
return {"status": "success", "message": "Tuyệt vời! Tất cả sản phẩm đều đã được gán tags."}
return {"status": "success", "message": "Không tìm thấy sản phẩm nào có clean_description."}
job_id = f"tags_{str(uuid.uuid4())[:8]}"
background_tasks.add_task(_process_batch_tags, job_id, codes)
return {
"status": "success",
"message": f"Tìm thấy {len(codes)} sản phẩm thiếu tags. Đang tiến hành tạo ẩn (Background Task).",
"message": f"Tìm thấy {len(codes)} sản phẩm. Đang tiến hành merge tags mới (occasion/season/style/fit/color) với tags cũ.",
"job_id": job_id,
"queued_codes": codes
}
......
......@@ -53,6 +53,45 @@ async def save_lead_turn(
lead_stage: dict | None = None,
) -> None:
"""Lưu một turn (human + AI) vào Postgres."""
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from common.sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(
"""
INSERT INTO canifa_chat.lead_flow_history
(device_id, conv_id, is_human, message)
VALUES (%s, %s, TRUE, %s)
""",
(device_id, conv_id, human_message),
)
ai_payload = json.dumps(
{
"ai_response": ai_response_text,
"products": products or [],
"lead_stage": lead_stage or {},
},
ensure_ascii=False,
)
mc.execute(
"""
INSERT INTO canifa_chat.lead_flow_history
(device_id, conv_id, is_human, message, ai_response, products, lead_stage)
VALUES (%s, %s, FALSE, %s, %s, %s::jsonb, %s::jsonb)
""",
(
device_id,
conv_id,
ai_payload,
ai_response_text,
json.dumps(products or [], ensure_ascii=False),
json.dumps(lead_stage or {}, ensure_ascii=False),
),
)
logger.debug(f"✅ [LeadFlowPG MOCK] Saved turn dev={device_id[:8]}… conv={conv_id[:8]}…")
return
pool = await get_pg_pool()
if not pool:
return
......@@ -110,6 +149,33 @@ async def get_lead_history(
Lấy history theo (device_id, conv_id).
Trả về list dict mới nhất trước (DESC).
"""
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from common.sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(
"""
SELECT id, is_human, message, created_at
FROM canifa_chat.lead_flow_history
WHERE device_id = %s AND conv_id = %s
ORDER BY id DESC
LIMIT %s
""",
(device_id, conv_id, limit),
)
rows = mc.fetchall()
return [
{
"id": r["id"],
"is_human": r["is_human"],
"message": r["message"],
"timestamp": r["created_at"] if isinstance(r["created_at"], str) else r["created_at"].isoformat(),
"conversation_id": conv_id,
}
for r in rows
]
pool = await get_pg_pool()
if not pool:
return []
......@@ -148,6 +214,38 @@ async def list_lead_conversations(device_id: str, limit: int = 20) -> list[dict]
"""
Lấy danh sách conversation của 1 device để populate dropdown.
"""
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from common.sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(
"""
SELECT conv_id,
MIN(created_at) AS started_at,
MAX(created_at) AS last_at,
COUNT(*) AS turn_count
FROM canifa_chat.lead_flow_history
WHERE device_id = %s
GROUP BY conv_id
ORDER BY last_at DESC
LIMIT %s
""",
(device_id, limit),
)
# Note: COUNT(*) FILTER (WHERE is_human) was translated to standard COUNT(*) for sqlite safety in this query temporarily.
# It's better to just use COUNT(*) because SQLite's FILTER support might be tricky with our translate regex.
rows = mc.fetchall()
return [
{
"conv_id": r["conv_id"],
"started_at": r["started_at"] if isinstance(r["started_at"], str) else r["started_at"].isoformat(),
"last_at": r["last_at"] if isinstance(r["last_at"], str) else r["last_at"].isoformat(),
"turn_count": r["turn_count"],
}
for r in rows
]
pool = await get_pg_pool()
if not pool:
return []
......@@ -189,6 +287,37 @@ async def get_all_history_for_dashboard(limit: int = 200) -> list[dict]:
"""
Admin: lấy toàn bộ history gần nhất (mọi device, mọi conv) cho dashboard.
"""
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from common.sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(
"""
SELECT id, device_id, conv_id, is_human, message,
ai_response, products, lead_stage, created_at
FROM canifa_chat.lead_flow_history
ORDER BY id DESC
LIMIT %s
""",
(limit,),
)
rows = mc.fetchall()
return [
{
"id": r["id"],
"device_id": str(r["device_id"])[:12] + "…",
"conv_id": r["conv_id"],
"is_human": r["is_human"],
"message": r["message"],
"ai_response": r["ai_response"],
"products": r["products"],
"lead_stage": r["lead_stage"],
"created_at": r["created_at"] if isinstance(r["created_at"], str) else r["created_at"].isoformat(),
}
for r in rows
]
pool = await get_pg_pool()
if not pool:
return []
......
......@@ -41,8 +41,9 @@ def get_pooled_connection_compat():
if not db_pool._pool:
db_pool.initialize()
import time
conn = None
for _ in range(3):
for attempt in range(3):
try:
conn = db_pool._pool.getconn()
# Perform a ping to ensure the TCP connection is still alive (handles AWS/Azure idle drop)
......@@ -55,6 +56,7 @@ def get_pooled_connection_compat():
conn.close() # Close it hard
except Exception:
pass
time.sleep(0.1 * (2 ** attempt))
# Final attempt if all retries hit a broken connection
conn = db_pool._pool.getconn()
......
......@@ -50,6 +50,13 @@ class PostgresReadonly:
@classmethod
async def execute_query_async(cls, sql: str) -> list[dict[str, Any]]:
"""Execute a SELECT query and return list of dicts."""
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from common.sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(sql)
return mc.fetchall()
pool = await cls._get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(sql)
......@@ -58,6 +65,13 @@ class PostgresReadonly:
@classmethod
async def execute_insert_async(cls, sql: str, *args) -> int:
"""Execute an INSERT query with args and return the new ID."""
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from common.sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(sql, args)
return mc._last_insert_id or 0
pool = await cls._get_pool()
async with pool.acquire() as conn:
result = await conn.fetchval(sql, *args)
......
......@@ -109,6 +109,28 @@ def translate_query(query: str) -> str:
# MockCursor — SYNCHRONOUS, dùng sqlite3 trực tiếp
# ---------------------------------------------------------------------------
class MockRow(tuple):
"""
Giả lập psycopg2 Row / aiomysql DictCursor kết hợp.
Hỗ trợ truy cập qua chỉ mục (row[0]) và qua tên cột (row['id']).
"""
def __new__(cls, cols, values):
return super().__new__(cls, values)
def __init__(self, cols, values):
self._dict = dict(zip(cols, values))
def __getitem__(self, key):
if isinstance(key, str):
return self._dict[key]
return super().__getitem__(key)
def keys(self):
return self._dict.keys()
def get(self, key, default=None):
return self._dict.get(key, default)
class MockCursor:
"""
Giả lập Psycopg2/3 Cursor, chạy ĐỒNG BỘ trên SQLite.
......@@ -144,7 +166,7 @@ class MockCursor:
fetched = cur.fetchall()
if cur.description:
cols = [d[0] for d in cur.description]
self._rows = [dict(zip(cols, row)) for row in fetched]
self._rows = [MockRow(cols, row) for row in fetched]
self.description = [(d[0],) for d in cur.description]
else:
self._rows = []
......@@ -155,7 +177,7 @@ class MockCursor:
if is_insert:
self._last_insert_id = cur.lastrowid
# Giả lập RETURNING id → trả về lastrowid dưới dạng hàng đầu tiên
self._rows = [(cur.lastrowid,)] if cur.lastrowid else []
self._rows = [MockRow(["id"], [cur.lastrowid])] if cur.lastrowid else []
else:
self._rows = []
except Exception as e:
......
......@@ -120,7 +120,8 @@ class StarRocksConnection:
from .sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(query, params)
return mc.fetchall()
# Convert MockRow (tuple) → dict so callers can mutate via p["key"] = val
return [dict(zip(row.keys(), row)) for row in mc.fetchall()]
# print(" [DB] 🚀 Bắt đầu truy vấn dữ liệu...")
# (Reduced noise in logs)
......@@ -144,11 +145,13 @@ class StarRocksConnection:
# Async pool shared
_shared_pool = None
_pool_lock = asyncio.Lock()
_pool_lock: asyncio.Lock | None = None
@classmethod
async def clear_pool(cls):
"""Clear and close existing pool (force recreate fresh connections)"""
if cls._pool_lock is None:
cls._pool_lock = asyncio.Lock()
async with cls._pool_lock:
if cls._shared_pool is not None:
logger.warning("🔄 Clearing StarRocks connection pool...")
......@@ -162,12 +165,15 @@ class StarRocksConnection:
Get or create shared async connection pool (Thread-safe singleton)
Optimized for cosine similarity queries (~200ms)
"""
if StarRocksConnection._pool_lock is None:
StarRocksConnection._pool_lock = asyncio.Lock()
if StarRocksConnection._shared_pool is None:
async with StarRocksConnection._pool_lock:
if StarRocksConnection._shared_pool is None:
logger.info(f"🔌 Creating Async Pool to {self.host}:{self.port}...")
minsize = int(os.getenv("STARROCKS_POOL_MINSIZE", "2"))
maxsize = int(os.getenv("STARROCKS_POOL_MAXSIZE", "80"))
maxsize = int(os.getenv("STARROCKS_POOL_MAXSIZE", "20"))
StarRocksConnection._shared_pool = await aiomysql.create_pool(
host=self.host,
port=self.port,
......@@ -196,7 +202,8 @@ class StarRocksConnection:
from .sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(query, params)
return mc.fetchall()
# Convert MockRow (tuple) → dict so callers can mutate via p["key"] = val
return [dict(zip(row.keys(), row)) for row in mc.fetchall()]
max_retries = 3
......
......@@ -93,8 +93,6 @@ PORT: int = int(os.getenv("PORT", "5000"))
FIRECRAWL_API_KEY: str | None = os.getenv("FIRECRAWL_API_KEY")
# ====================== LANGFUSE CONFIGURATION (DEPRECATED) ======================
LANGFUSE_SECRET_KEY: str | None = os.getenv("LANGFUSE_SECRET_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")
# ====================== LANGSMITH CONFIGURATION (TẮT VÌ RATE LIMIT) ======================
......
{"pending": {"magento_ref_code": "5TS25S021-SR079", "product_name": "\u00c1o ph\u00f4ng unisex ng\u01b0\u1eddi l\u1edbn C\u1edd \u0111\u1ecf sao v\u00e0ng"}, "tags_missing": 1335}
\ No newline at end of file
{"duplicates": [], "orphans": 228, "sr_total": 1738}
\ No newline at end of file
{"missing_descriptions_in_sr": 36}
\ No newline at end of file
{"tags": ["[\"M\u00e0u H\u1ed3ng/ Pink- Magenta\", \"T\u00f4ng s\u00e1ng (Light)\", \"M\u00e0u Tr\u1eafng/ White\", \"T\u00f4ng trung t\u00ednh (Neutral)\", \"M\u00e0u Be/ Beige\", \"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"\u0110i h\u1ecdc\", \"D\u1ec5 th\u01b0\u01a1ng\", \"N\u0103ng \u0111\u1ed9ng\", \"Th\u1ea5m h\u00fat\"]", "[\"M\u00e0u \u0110\u1ecf/ Red\", \"T\u00f4ng n\u1ed5i b\u1eadt (Dark)\", \"M\u00e0u H\u1ed3ng/ Pink- Magenta\", \"T\u00f4ng s\u00e1ng (Light)\", \"M\u00f9a \u0111\u00f4ng\"]", "[\"M\u00e0u N\u00e2u/ Brown\", \"T\u00f4ng trung t\u00ednh (Neutral)\", \"M\u00e0u X\u00e1m/ Gray\", \"M\u00e0u \u0110en/ Black\", \"M\u00e0u Tr\u1eafng/ White\", \"M\u00f9a h\u00e8\", \"\u0110i ch\u01a1i / d\u1ea1o ph\u1ed1\", \"C\u01a1 b\u1ea3n (Basic)\", \"Regular\"]", "[\"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"\u0110i ch\u01a1i / d\u1ea1o ph\u1ed1\", \"C\u01a1 b\u1ea3n (Basic)\", \"Relaxed\"]", "[\"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"M\u1eb7c nh\u00e0\", \"C\u01a1 b\u1ea3n (Basic)\"]", "[\"M\u00e0u Be/ Beige\", \"T\u00f4ng trung t\u00ednh (Neutral)\", \"M\u00e0u X\u00e1m/ Gray\", \"M\u00e0u Tr\u1eafng/ White\", \"M\u00e0u \u0110en/ Black\", \"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"\u0110i l\u00e0m / C\u00f4ng s\u1edf\", \"\u0110i ch\u01a1i / d\u1ea1o ph\u1ed1\", \"T\u1ed1i gi\u1ea3n\", \"Th\u1ea5m h\u00fat\"]", "[\"M\u00e0u Tr\u1eafng/ White\", \"T\u00f4ng trung t\u00ednh (Neutral)\", \"M\u00f9a \u0111\u00f4ng\", \"\u0110i l\u00e0m / C\u00f4ng s\u1edf\", \"\u0110i ch\u01a1i / d\u1ea1o ph\u1ed1\"]", "[\"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"M\u1eb7c nh\u00e0\", \"C\u01a1 b\u1ea3n (Basic)\", \"Slim\"]", "[\"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"\u0110i ch\u01a1i / d\u1ea1o ph\u1ed1\", \"D\u1ec5 th\u01b0\u01a1ng\", \"Regular\"]", "[\"M\u00f9a h\u00e8\", \"M\u00f9a \u0111\u00f4ng\", \"M\u1eb7c nh\u00e0\", \"C\u01a1 b\u1ea3n (Basic)\", \"Slim\"]"]}
\ No newline at end of file
{
"status": "success",
"total": 1930,
"total_sr": 1738,
"has_desc": 1930,
"approved": 1929,
"pending": 1,
"missing": 0,
"has_clean_desc": 1928,
"missing_clean_desc": 18,
"missing_tags": 0,
"has_size_guide": 933,
"progress": 99.9,
"db_stats": {
"enriched": 1460,
"raw_only": 0,
"last_updated": "2026-04-29 06:55:41"
}
}
\ No newline at end of file
......@@ -7,6 +7,7 @@ import uvicorn
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
from api.main_router import api_router
from common.cache import redis_cache
......@@ -35,15 +36,8 @@ if not os.path.exists(STATIC_DIR):
print(f"[OK] Static dir resolved: {STATIC_DIR}")
app = FastAPI(
title="Contract AI Service",
description="API for Contract AI Service",
version="1.0.0",
)
@app.on_event("startup")
async def startup_event():
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize Redis cache, EventBus, and start background workers."""
await redis_cache.initialize()
logger.info("✅ Redis cache initialized")
......@@ -58,9 +52,12 @@ async def startup_event():
asyncio.create_task(report_worker_loop())
logger.info("✅ Report Queue Worker started (background task)")
# ─── Start publish engine background loop ───────────────────────────────────
from common.social.scheduler import start_publish_engine
start_publish_engine(app) # Auto-publish scheduled content every 30s
yield
@app.on_event("shutdown")
async def shutdown_event():
"""Cleanup resources before exit to prevent connection leaks during hot-reload."""
# Stop FastStream EventBus
await event_bus.stop()
......@@ -80,6 +77,14 @@ async def shutdown_event():
pass
app = FastAPI(
title="Contract AI Service",
description="API for Contract AI Service",
version="1.0.0",
lifespan=lifespan,
)
@app.get("/")
async def root():
return RedirectResponse(url="/home/")
......@@ -136,9 +141,7 @@ middleware_manager.setup(
# api include
app.include_router(api_router)
# ─── Start publish engine background loop ───────────────────────────────────
from common.social.scheduler import start_publish_engine
start_publish_engine(app) # Auto-publish scheduled content every 30s
if __name__ == "__main__":
......
......@@ -42,7 +42,7 @@ body {
.cookbook-sidebar {
width: 300px;
background: var(--cb-sidebar-bg);
border-right: 1px solid var(--cb-border);
border-left: 1px solid var(--cb-border);
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
......@@ -392,12 +392,13 @@ pre {
.hamburger { display: block; }
.cookbook-sidebar {
position: fixed;
left: 0; top: 0; bottom: 0;
transform: translateX(-100%);
right: 0; top: 0; bottom: 0;
left: auto;
transform: translateX(100%);
}
.cookbook-sidebar.open {
transform: translateX(0);
box-shadow: 10px 0 30px rgba(0,0,0,0.5);
box-shadow: -10px 0 30px rgba(0,0,0,0.5);
}
.recipe-content {
padding: 20px;
......
......@@ -12,35 +12,28 @@
</head>
<body class="premium-dark">
<div class="cookbook-container">
<!-- Sidebar -->
<aside class="cookbook-sidebar" id="sidebar">
<div class="sidebar-header">
<div class="search-container">
<input type="text" id="recipeSearch" placeholder="Tìm kiếm recipe...">
<span class="search-icon">🔍</span>
</div>
</div>
<nav class="recipe-nav" id="recipeNav">
<!-- Loaded via JS -->
</nav>
</aside>
<!-- Main Content -->
<main class="cookbook-main">
<header class="content-header">
<button id="menuToggle" class="hamburger"></button>
<button id="menuToggle" class="hamburger">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
</button>
<div class="breadcrumb" id="breadcrumb">
<span>Cookbook</span>
</div>
<div class="header-actions">
<button id="themeToggle" class="btn-icon" title="Đổi giao diện">🌓</button>
<button id="downloadDocx" class="btn-icon" title="Tải DOCX">📄</button>
<button id="themeToggle" class="btn-icon" title="Đổi giao diện">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"></path></svg>
</button>
<button id="downloadDocx" class="btn-icon" title="Tải DOCX">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
</button>
</div>
</header>
<article class="recipe-content" id="recipeContent">
<div class="welcome-screen">
<h1>📖 Canifa AI Platform Cookbook</h1>
<h1><svg style="vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>Canifa AI Platform Cookbook</h1>
<p>Hướng dẫn chi tiết A-Z về kiến trúc, API và cách tích hợp các module trong hệ thống.</p>
<div class="stats-overview">
<div class="stat-card">
......@@ -59,10 +52,25 @@
</div>
</article>
</main>
<!-- Sidebar -->
<aside class="cookbook-sidebar" id="sidebar">
<div class="sidebar-header">
<div class="search-container">
<input type="text" id="recipeSearch" placeholder="Tìm kiếm recipe...">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</div>
</div>
<nav class="recipe-nav" id="recipeNav">
<!-- Loaded via JS -->
</nav>
</aside>
</div>
<!-- Floating Buttons -->
<button id="backToTop" class="back-to-top" title="Quay lại đầu trang"></button>
<button id="backToTop" class="back-to-top" title="Quay lại đầu trang">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
</button>
<!-- Zoom Overlay -->
<div id="zoomOverlay" class="zoom-overlay">
......
......@@ -37,7 +37,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const titleEl = document.createElement('div');
titleEl.className = 'nav-group-title';
titleEl.innerHTML = `<span>${group.name}</span> <span class="toggle-icon"></span>`;
titleEl.innerHTML = `<span>${group.name}</span> <span class="toggle-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>`;
const listEl = document.createElement('ul');
listEl.className = 'recipe-list';
......@@ -60,7 +60,9 @@ document.addEventListener('DOMContentLoaded', async () => {
titleEl.onclick = () => {
const isHidden = listEl.style.display === 'none';
listEl.style.display = isHidden ? 'block' : 'none';
titleEl.querySelector('.toggle-icon').textContent = isHidden ? '▼' : '▶';
titleEl.querySelector('.toggle-icon').innerHTML = isHidden
? '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'
: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>';
};
});
}
......
......@@ -4,24 +4,39 @@
"diagram": "diagrams/platform-architecture.svg",
"sections": [
{
"title": "Cấu trúc 5 lớp (5-Layer Stack)",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "Hệ thống được thiết kế theo mô hình micro-modular monolith, cho phép mở rộng nhanh chóng các tính năng AI mà không làm ảnh hưởng đến lõi hệ thống.<br>1. <b>Client Layer:</b> Trình duyệt web chạy <code>main.html</code>, quản lý layout chính và sidebar.<br>2. <b>Interface Layer:</b> Hệ thống iframe cho phép load các module độc lập, giúp cô lập CSS/JS.<br>3. <b>Gateway Layer:</b> FastAPI Router đóng vai trò điều phối tất cả các request API.<br>4. <b>Module Layer:</b> 36+ module nghiệp vụ (AI Agents, Product, Social, v.v.) xử lý logic cụ thể.<br>5. <b>Data Layer:</b> Kết hợp SQLite (local dev), StarRocks (olap), và các dịch vụ external."
"content": "Canifa AI Platform được thiết kế theo mô hình <b>Micro-modular Monolith</b>. Mô hình này giúp hệ thống dễ dàng triển khai như một khối thống nhất (Monolith) nhưng lại giữ được sự tách biệt logic của từng tính năng (Micro-modular), cho phép mở rộng nhanh chóng các tính năng AI mà không làm ảnh hưởng đến lõi hệ thống."
},
{
"title": "Danh sách API Modules chính",
"title": "2. Kiến trúc & Luồng dữ liệu (5-Layer Stack)",
"type": "text",
"content": "Hệ thống chia làm 5 lớp rõ rệt:<br><br><b>1. Client Layer:</b> Trình duyệt web chạy <code>main.html</code> đóng vai trò App Shell, quản lý Navigation Sidebar, Global State (Auth Token) và Light/Dark mode.<br><b>2. Interface Layer:</b> Hệ thống sử dụng Iframe để tải các Micro-frontend (MFE). Nhờ vậy, CSS/JS của từng tính năng (Chatbot, Dashboard, Social) hoàn toàn bị cô lập, không gây conflict.<br><b>3. Gateway Layer:</b> FastAPI Router điều phối tất cả các API requests, đi qua các Middleware (Rate Limit, Auth Guard, CORS).<br><b>4. Module Layer:</b> Chứa 36+ module nghiệp vụ. Các module giao tiếp với nhau thông qua Dependency Injection và EventBus (FastStream).<br><b>5. Data Layer:</b> Database lai giữa SQLite (dành cho Local Dev & Metadata nhanh) và StarRocks/Postgres (Olap Analytics cho dữ liệu lớn)."
},
{
"title": "3. Core Logic: Khởi chạy hệ thống (Lifespan)",
"type": "text",
"content": "Thay vì dùng <code>@app.on_event</code> kiểu cũ, hệ thống đã chuyển sang chuẩn <b>FastAPI Lifespan</b> (context manager) để quản lý Graceful Startup và Shutdown. Tại đây, các resource như Redis Cache, EventBus, Background Tasks (Social Publisher) sẽ được khởi tạo cẩn thận và đóng an toàn khi server stop, tránh hiện tượng rò rỉ bộ nhớ (Memory Leak) hoặc treo worker (Zombie process)."
},
{
"title": "4. Code Snippet Thực Tế (server.py)",
"type": "code",
"language": "python",
"code": "from contextlib import asynccontextmanager\nfrom fastapi import FastAPI\nfrom common.cache import redis_cache\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n # 1. Khởi tạo Redis cache\n await redis_cache.initialize()\n logger.info(\"✅ Redis cache initialized\")\n\n # 2. Start EventBus & Background workers\n await event_bus.start()\n start_publish_engine(app)\n \n yield # Server bắt đầu phục vụ requests\n\n # 3. Cleanup khi Shutdown (Ctrl+C hoặc Reload)\n await event_bus.stop()\n db_pool.close_all()\n logger.info(\"🛑 Postgres Connection Pool nicely closed\")\n\napp = FastAPI(\n title=\"Canifa AI Platform\",\n lifespan=lifespan,\n)"
},
{
"title": "5. Danh sách API Modules quan trọng",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/auth/me", "description": "Lấy thông tin user hiện tại và settings LLM." },
{ "method": "POST", "path": "/api/chat", "description": "Endpoint chính cho chatbot tương tác với n8n." },
{ "method": "GET", "path": "/api/live-monitor", "description": "Theo dõi các event realtime qua WebSocket." }
{ "method": "GET", "path": "/api/auth/me", "description": "Lấy thông tin User Session và cài đặt LLM Key cá nhân." },
{ "method": "POST", "path": "/api/chat", "description": "Endpoint chính xử lý Chatbot reasoning với n8n." },
{ "method": "GET", "path": "/api/live-monitor", "description": "WebSocket endpoint bắn log Realtime." }
]
},
{
"title": "Cấu trúc Thư mục",
"type": "code",
"language": "bash",
"code": "backend/\n├── api/ # Chứa toàn bộ logic router & business\n├── database/ # SQLite files & Migrations\n├── common/ # Shared utilities (db, auth, logs)\n├── static/ # Frontend assets (HTML, CSS, JS)\n└── server.py # Entry point của ứng dụng"
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi không load được ảnh/SVG:</b><br>Khi code frontend, các file SVG phải được escape ký tự <code>&amp;</code> chuẩn xác, nếu không trình duyệt sẽ block không cho hiển thị. Static files được serve qua route <code>/static/{file_path:path}</code>, do đó các URL tương đối trong Iframe sẽ tự động map đúng về thư mục <code>static/</code>.<br><br><b>🚫 Lỗi treo Port (Zombie Process):</b><br>Nếu dùng uvicorn reload và gặp lỗi Port in use, nguyên nhân là do Background Tasks hoặc DB Connections không được đóng đàng hoàng ở block <code>yield</code> của Lifespan. Luôn phải gọi <code>.close()</code> trong hàm shutdown."
}
]
}
......@@ -4,20 +4,35 @@
"diagram": "diagrams/database-layer.svg",
"sections": [
{
"title": "Cơ chế SQL Translation",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "Để hỗ trợ chạy local mà không cần Postgres/StarRocks, module <code>sqlite_mock.py</code> thực hiện đánh chặn các câu lệnh SQL và dịch chúng sang dialect của SQLite thông qua hàm <code>translate_query()</code>. Các quy tắc chính bao gồm:<br>- Chuyển <code>%s</code> thành <code>?</code><br>- Thay thế tên schema (ví dụ: <code>public.users</code> thành <code>pg__public__users</code>)<br>- Giả lập các hàm đặc thù như <code>ANY_VALUE</code>, <code>MAX_BY</code>, <code>ILIKE</code>."
"content": "Module Database cung cấp một lớp <b>Wrapper</b> thông minh để ứng dụng có thể chạy mượt mà trên cả môi trường <b>Local (SQLite)</b> và <b>Production (Postgres/StarRocks)</b> mà không cần phải viết lại các câu SQL query. Nó đặc biệt hữu ích khi Dev muốn debug thuật toán phối đồ (Fashion Match) hoặc luồng sinh mô tả (Ultra Description) tại máy cá nhân mà không có kết nối tới Data Warehouse."
},
{
"title": "Async vs Sync Paths",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "1. <b>Async Path:</b> Sử dụng <code>aiosqlite</code> cho các API route hiện đại. Luồng này truy cập trực tiếp vào file <code>canifa_ai_dump.sqlite</code>.<br>2. <b>Sync Path:</b> Sử dụng <code>MockCursor</code> để tương thích với các legacy helper classes (như UltraDescDB) mà không cần refactor hàng chục method sang async."
"content": "Luồng kết nối Database chia thành hai hướng chính (Sync và Async):<br><br><b>A. Async Path (Tuyến chính):</b> Sử dụng <code>aiosqlite</code> (nếu chạy local) hoặc <code>aiomysql / asyncpg</code> (trên Prod). Luồng này được dùng trực tiếp ở các FastAPI route hiện đại, đảm bảo tốc độ cao, không block thread.<br><b>B. Sync Path (Tuyến Legacy):</b> Vẫn sử dụng code tuần tự. Để tương thích với các Class cũ như <code>UltraDescDB</code>, hệ thống sinh ra một lớp <code>MockCursor</code> để bọc và \"giả lập\" hành vi của psycopg2."
},
{
"title": "Cấu hình Kết nối",
"title": "3. Core Logic: Cơ chế SQL Translation",
"type": "text",
"content": "Vì cú pháp SQL của Postgres và SQLite khác nhau, file <code>sqlite_mock.py</code> đóng vai trò là một <b>SQL Interceptor</b>, nó chặn các câu query được gửi từ code Python, biên dịch lại rồi mới đẩy xuống SQLite. Các quy tắc chính bao gồm:<br>- Thay placeholder <code>%s</code> thành <code>?</code><br>- Thay schema name: ví dụ <code>public.users</code> -> <code>pg__public__users</code><br>- Định dạng lại cấu trúc JSON: map các hàm như <code>ANY_VALUE()</code> sang <code>MIN()</code>."
},
{
"title": "4. Core Logic: Connection Pool & Thundering Herd Protection",
"type": "text",
"content": "Hệ thống triển khai <b>Exponential Backoff</b> bên trong <code>pool_wrapper.py</code>. Khi Postgres chập chờn hoặc khởi động lại, thay vì hàng nghìn requests dội vào xin kết nối cùng lúc gây chết DB (Thundering herd), hệ thống sẽ chủ động ngắt connection lỗi và sleep tăng dần thời gian (<code>time.sleep(0.1 * (2 ** attempt))</code>) trước khi thử lại."
},
{
"title": "5. Code Snippet Thực Tế (pool_wrapper.py)",
"type": "code",
"language": "python",
"code": "from common.pool_wrapper import get_pooled_connection_compat\n\n# Cách lấy connection tương thích cả Prod (PG) và Dev (SQLite Mock)\nwith get_pooled_connection_compat() as conn:\n with conn.cursor() as cur:\n cur.execute(\"SELECT * FROM public.users WHERE id = %s\", (user_id,))\n user = cur.fetchone()"
"code": "from common.pool_wrapper import get_pooled_connection_compat\nimport time\n\ndef fetch_user_data(user_id):\n # Connection pool tự động handle fallback và backoff\n for attempt in range(3):\n try:\n with get_pooled_connection_compat() as conn:\n with conn.cursor() as cur:\n # Lớp Mock sẽ tự động dịch `%s` thành `?` nếu chạy Local SQLite\n cur.execute(\"SELECT * FROM public.users WHERE id = %s\", (user_id,))\n return cur.fetchone()\n except Exception as e:\n time.sleep(0.1 * (2 ** attempt)) # Exponential Backoff\n return None"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi `sqlite3.OperationalError: no such table`</b><br>Khi query các bảng nằm ngoài schema mặc định (ví dụ schema <code>canifa_ai</code>), bạn phải đảm bảo đã import data vào file <code>canifa_ai_dump.sqlite</code> và prefix tên bảng thành <code>canifa_ai__tablename</code>.<br><br><b>🚫 Lỗi khóa DB (Database is locked)</b><br>Do SQLite không hỗ trợ ghi đồng thời tốt như Postgres, nếu có nhiều Background Tasks hoặc Async Coroutines cùng thực thi lệnh <code>UPDATE</code> / <code>INSERT</code>, SQLite sẽ báo lỗi locked. Để giảm thiểu, luôn dùng <code>with conn:</code> để commit/rollback tự động và trả kết nối về pool nhanh nhất có thể."
}
]
}
......@@ -4,25 +4,40 @@
"diagram": "diagrams/chatbot-pipeline.svg",
"sections": [
{
"title": "Luồng xử lý (Execution Flow)",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "1. <b>Chat Controller:</b> Tiếp nhận request, kiểm tra Redis cache. Nếu miss, khởi tạo LangGraph engine.<br>2. <b>LangGraph Agent:</b> Chạy vòng lặp suy nghĩ (Reasoning Loop). Agent quyết định có cần gọi công cụ (tools) hay không.<br>3. <b>Tool Execution:</b> Nếu cần dữ liệu, Agent gọi các API chuyên dụng (n8n API) để lấy thông tin sản phẩm, tồn kho, cửa hàng.<br>4. <b>Response Synthesis:</b> Kết hợp dữ liệu thô từ tools và kiến thức thời trang để tạo câu trả lời tự nhiên."
"content": "Chatbot Core là trái tim của hệ thống Canifa AI Stylist. Thay vì chỉ hỏi/đáp LLM thông thường, hệ thống tích hợp sâu với <b>n8n (Workflow Automation)</b> để xây dựng luồng Agentic. AI Agent có khả năng tự động hiểu ngữ cảnh, gọi các tool nội bộ (Check tồn kho, Tìm ảnh sản phẩm, Gợi ý size) và phản hồi theo đúng Persona của một Stylist."
},
{
"title": "Danh sách API n8n Tools",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "Quy trình xử lý một câu hỏi của khách hàng đi qua 4 bước:<br><br><b>1. Chat Controller:</b> Tiếp nhận request từ API <code>/api/chat</code>. Kiểm tra Token, Rate Limit và Redis Cache. Nếu cache hit (câu hỏi lặp lại), trả kết quả ngay.<br><b>2. Reasoning Loop (LangGraph/ReAct):</b> Agent bắt đầu vòng lặp suy nghĩ (Thought -> Action -> Observation). Tại bước Action, nó quyết định cần sử dụng công cụ (Tool) nào để giải quyết vấn đề.<br><b>3. Tool Execution (n8n API):</b> Nếu cần dữ liệu, Agent gọi các REST API chuyên dụng do n8n cung cấp (ví dụ: lấy list sản phẩm, check size thực tế).<br><b>4. Response Synthesis:</b> Sau khi gom đủ Observation từ các Tools, LLM tổng hợp lại thành một câu tư vấn hoàn chỉnh, tự nhiên và gán kèm ảnh sản phẩm."
},
{
"title": "3. Core Logic: Guardrails & Prompt Injection",
"type": "text",
"content": "Để ngăn chặn các rủi ro bảo mật (như Prompt Injection) hoặc AI bị ảo giác (Hallucination), hệ thống áp dụng các <b>Guardrails</b> khắt khe:<br>- <b>Product Verification:</b> AI không được phép tự bịa ra sản phẩm. Nếu n8n tool trả về rỗng, AI phải báo hết hàng.<br>- <b>Demographic Neutrality:</b> Không thiên vị giới tính, không phán xét dáng người."
},
{
"title": "4. Danh sách API n8n Tools",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/agent/n8n/products", "description": "Lấy mẫu sản phẩm hoặc tìm kiếm theo SKU/tên." },
{ "method": "GET", "path": "/api/agent/n8n/stock", "description": "Kiểm tra tồn kho realtime của Canifa." },
{ "method": "GET", "path": "/api/agent/n8n/stores", "description": "Tìm kiếm danh sách cửa hàng theo khu vực." },
{ "method": "GET", "path": "/api/agent/n8n/knowledge", "description": "Tra cứu chính sách đổi trả, bảng size (RAG)." }
{ "method": "GET", "path": "/api/agent/n8n/products", "description": "Tìm kiếm sản phẩm theo semantic search hoặc keywords." },
{ "method": "GET", "path": "/api/agent/n8n/stock", "description": "Kiểm tra tồn kho realtime của mã SKU." },
{ "method": "GET", "path": "/api/agent/n8n/stores", "description": "Tìm kiếm danh sách cửa hàng còn hàng theo khu vực." },
{ "method": "GET", "path": "/api/agent/n8n/knowledge", "description": "Tra cứu RAG (Chính sách đổi trả, bảng size...)." }
]
},
{
"title": "Cấu trúc Request",
"title": "5. Code Snippet Thực Tế (LangGraph State)",
"type": "code",
"language": "json",
"code": "{\n \"user_query\": \"Tìm cho mình áo phông nam màu xanh size L còn hàng ở Hà Nội\",\n \"images\": [],\n \"history_limit\": 15\n}"
"language": "python",
"code": "from typing import TypedDict, Annotated, Sequence\nfrom langchain_core.messages import BaseMessage\nimport operator\n\nclass AgentState(TypedDict):\n # Lưu trữ lịch sử hội thoại\n messages: Annotated[Sequence[BaseMessage], operator.add]\n # Lưu thông tin context user (user_id, intent)\n user_context: dict\n # Trạng thái các tool đã gọi\n tool_history: list[str]\n\ndef reasoning_node(state: AgentState):\n \"\"\"Node chính gọi LLM để quyết định hành động tiếp theo\"\"\"\n prompt = system_prompt.format(context=state[\"user_context\"])\n response = llm.invoke([prompt] + state[\"messages\"])\n return {\"messages\": [response]}\n"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi Context Window Quá Dài:</b><br>Nếu user chat quá nhiều, LLM sẽ báo lỗi vượt quá số lượng token cho phép. Hệ thống hiện tại có cơ chế <code>history_limit</code> và <b>Conversation Truncation</b> để cắt bớt các đoạn hội thoại cũ (chỉ giữ lại 15 tin nhắn gần nhất).<br><br><b>🚫 Lỗi Tool Timeout:</b><br>Khi n8n xử lý quá chậm hoặc StarRocks delay, API n8n sẽ timeout. Agent cần có khả năng bắt exception này và trả lời khéo léo (VD: <i>'Hệ thống đang quá tải, anh vui lòng đợi chút nhé'</i>) thay vì văng HTTP 500."
}
]
}
{
"title": "Prompt & Tool Management",
"description": "Hướng dẫn cách quản lý System Prompt và các Tool Prompt linh hoạt mà không cần khởi động lại server.",
"title": "Prompt System & LLM Factory",
"description": "Hướng dẫn cách quản lý System Prompt và các Tool Prompt linh hoạt mà không cần khởi động lại server, đi kèm kiến trúc LLM Factory.",
"diagram": "diagrams/prompt-management.svg",
"sections": [
{
"title": "Cơ chế Dynamic Prompt",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "Hệ thống lưu trữ prompt trong các file <code>.txt</code> tại thư mục <code>backend/agent/</code>. Mỗi khi Admin cập nhật prompt qua UI, server sẽ ghi đè vào file tương ứng và gọi hàm <code>reset_chain_cache()</code> để xóa cache logic của LangChain. Lần truy vấn kế tiếp, Agent sẽ tự động nhận diện sự thay đổi nội dung (via MD5 hash) và nạp lại prompt mới."
"content": "Prompt Engineering là linh hồn của AI Stylist. Để giúp Admin (Product Owner) có thể dễ dàng tuning prompt theo từng chiến dịch (Ví dụ: Mùa Đông thì push mạnh Áo Khoác), hệ thống tách bạch toàn bộ Prompt ra khỏi code. Các file Prompt được lưu dưới dạng <code>.txt</code> tại <code>backend/agent/</code> và có API để Hot-reload mà không phải restart Uvicorn."
},
{
"title": "Phân loại Prompt",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "1. <b>System Prompt:</b> Định nghĩa nhân cách, kiến thức cốt lõi và phong cách trả lời của Agent.<br>2. <b>Tool Prompts:</b> Định nghĩa chi tiết cách Agent sử dụng từng công cụ (ví dụ: cách tìm sản phẩm, cách tra cứu tồn kho).<br>3. <b>User Insight Template:</b> Định nghĩa các thông tin cần thu thập về người dùng (giới tính, sở thích, size)."
"content": "Quy trình update prompt diễn ra như sau:<br><br><b>1. UI Edit:</b> Admin sửa prompt trên giao diện Dashboard.<br><b>2. File Override:</b> API <code>POST /api/agent/system-prompt</code> sẽ ghi đè nội dung mới vào file <code>.txt</code>.<br><b>3. MD5 Cache Invalidation:</b> LLM Chain không đọc file từ ổ cứng liên tục (để tối ưu I/O). Thay vào đó, mỗi khi hàm <code>reset_chain_cache()</code> được gọi, nó sẽ hash MD5 file prompt. Nếu hash thay đổi, hệ thống sẽ xóa instance LLM cũ khỏi RAM và re-compile lại LangChain.<br><b>4. LLM Factory:</b> File <code>llm_factory.py</code> chịu trách nhiệm nạp Prompt, đọc API keys, chọn Model (GPT-4o, Gemini Flash-Lite) và tiêm các tham số động (như <code>{current_date}</code>) vào ngữ cảnh."
},
{
"title": "API Quản lý Prompt",
"title": "3. Phân loại Prompt",
"type": "text",
"content": "<b>- System Prompt:</b> Định nghĩa Persona (Chuyên gia thời trang, Lịch sự, Nhiệt tình). Cung cấp luật cấm (Không chê bai ngoại hình khách hàng).<br><b>- Tool Prompts:</b> Hướng dẫn AI cách dùng Tool (VD: <i>\"Chỉ dùng API kiểm tra tồn kho khi user hỏi size cụ thể\"</i>).<br><b>- User Insight Template:</b> Prompt nội bộ dùng để trích xuất thông tin khách hàng từ lịch sử chat (nhằm lưu vào DB Profiling)."
},
{
"title": "4. API Quản lý Prompt",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/agent/system-prompt", "description": "Lấy nội dung System Prompt hiện tại." },
{ "method": "POST", "path": "/api/agent/system-prompt", "description": "Cập nhật System Prompt và làm mới cache." },
{ "method": "GET", "path": "/api/agent/system-prompt", "description": "Lấy nội dung System Prompt hiện tại để view trên UI." },
{ "method": "POST", "path": "/api/agent/system-prompt", "description": "Cập nhật System Prompt và trigger làm mới cache." },
{ "method": "GET", "path": "/api/agent/tool-prompts", "description": "Liệt kê danh sách các tool prompt có sẵn." },
{ "method": "POST", "path": "/api/agent/tool-prompts/{filename}", "description": "Cập nhật nội dung một tool prompt cụ thể." }
]
},
{
"title": "5. Code Snippet Thực Tế (Dynamic Parameters)",
"type": "code",
"language": "python",
"code": "from datetime import datetime\nfrom langchain.prompts import ChatPromptTemplate\n\ndef get_compiled_prompt(raw_prompt_text: str):\n # Tiêm các biến thời gian thực vào Prompt để AI nhận thức được thời gian\n today_str = datetime.now().strftime(\"%A, %Y-%m-%d\")\n season = get_current_season() # Xử lý logic mùa (Xuân, Hè...)\n \n injected_prompt = raw_prompt_text.replace(\n \"{context_block}\",\n f\"Hôm nay là {today_str}. Hiện tại đang là bộ sưu tập {season}.\"\n )\n return ChatPromptTemplate.from_messages([\n (\"system\", injected_prompt),\n (\"placeholder\", \"{chat_history}\"),\n (\"human\", \"{input}\"),\n (\"placeholder\", \"{agent_scratchpad}\"),\n ])"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi Prompts Mất Cập Nhật (Stale Cache):</b><br>Do LangChain cache đối tượng <code>Runnable</code>, nếu sửa file TXT bằng tay (nano/vim) trên server mà không call API, hệ thống sẽ KHÔNG biết prompt bị đổi để nạp lại. Luôn sửa qua API hoặc restart server.<br><br><b>🚫 Lỗi Phá Vỡ JSON Schema ở Tool Prompt:</b><br>Nếu Tool Prompt yêu cầu AI trả về JSON mà Admin lỡ tay xóa câu <i>\"Must output strictly valid JSON\"</i>, AI có thể sinh ra text lộn xộn khiến pydantic parser crash. Khi edit prompt, phải giữ nguyên các chỉ thị kỹ thuật (Technical constraints)."
}
]
}
......@@ -4,24 +4,39 @@
"diagram": "diagrams/ultra-desc-pipeline.svg",
"sections": [
{
"title": "Quy trình AI Generation",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "1. <b>Giai đoạn Vision:</b> Sử dụng model <b>Llama-4-Scout</b> để phân tích ảnh sản phẩm, trích xuất 28 trường dữ liệu thô (màu sắc, thiết kế, túi, cổ áo, v.v.).<br>2. <b>Giai đoạn Enrichment:</b> Sử dụng <b>GPT-OSS 120B</b> để viết lại nội dung theo phong cách chuyên gia Stylist, bổ sung hướng dẫn bảo quản và mix-match.<br>3. <b>Persistance:</b> Lưu trữ kết quả vào bảng <code>ultra_descriptions</code> (Postgres) và đánh dấu trạng thái <i>Pending</i> để chờ Admin duyệt."
"content": "Ultra Description là module sinh mô tả sản phẩm tự động chuẩn SEO và phục vụ RAG cho Chatbot. Khác với cách dùng 1 prompt đơn giản, hệ thống này chia thành nhiều giai đoạn (Multi-stage Pipeline) để phân tích ảnh (Vision) rồi mới dùng Text LLM để viết văn (Enrichment). Điều này giúp tránh bị \"ảo giác\" màu sắc hoặc thiết kế."
},
{
"title": "Cấu trúc 28 trường dữ liệu",
"type": "code",
"language": "json",
"code": "{\n \"ten_san_pham\": \"Áo Phông Nam Có Túi\",\n \"chat_lieu\": \"100% Cotton\",\n \"phoi_do\": \"Quần Jean + Sneaker\",\n \"dip_mac\": \"Đi chơi · Đi làm\",\n \"faq_1_q\": \"Áo có bị phai màu không?\",\n \"faq_1_a\": \"Chất liệu Cotton USA cao cấp giúp giữ màu bền bỉ...\"\n}"
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "Quá trình sinh mô tả 1 sản phẩm tốn khoảng 5-10s, gồm các bước:<br><br><b>1. Vision Analysis:</b> Model Vision nhận link ảnh sản phẩm, bóc tách ra 28 trường dữ liệu kỹ thuật (Kiểu cổ, Loại tay, Độ dài, Chất liệu...).<br><b>2. Semantic Check:</b> Đối chiếu dữ liệu Vision với Master Data từ ERP (Ví dụ: Vision bảo màu đỏ, nhưng ERP mã màu là Xanh thì ưu tiên ERP).<br><b>3. Enrichment (Copywriting):</b> Truyền 28 trường kỹ thuật này vào Text Model (Gemini Flash-Lite hoặc Llama) để sinh ra văn phong tư vấn Stylist (Mix-match, FAQ, Dip mac...).<br><b>4. Persistance:</b> Dữ liệu được lưu vào bảng <code>ultra_descriptions</code> với trạng thái <i>Pending</i> (Chờ Admin duyệt trên UI)."
},
{
"title": "3. Core Logic: Seasonal Tagging",
"type": "text",
"content": "Một trong những cải tiến lớn là <b>Deterministic Seasonal Tagging</b>. Thay vì để LLM tự đoán mùa (dễ sai), hệ thống mapping cứng trường <code>season</code> từ Database sang tags (Ví dụ: `Spring-Summer` -> `Mùa Hè, Thoáng mát`). LLM bị ép (Enforced) phải dùng các tags này."
},
{
"title": "API Quản lý Mô tả",
"title": "4. Danh sách API Quản lý Mô tả",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/product-desc/overview", "description": "Xem thống kê độ phủ của mô tả AI." },
{ "method": "POST", "path": "/api/product-desc/generate", "description": "Sinh mô tả mới cho 1 sản phẩm cụ thể." },
{ "method": "POST", "path": "/api/product-desc/batch-generate", "description": ưa danh sách sản phẩm vào queue xử lý hàng loạt." }
{ "method": "GET", "path": "/api/product-desc/overview", "description": "Lấy tỷ lệ Coverage: Bao nhiêu SP đã có mô tả AI." },
{ "method": "POST", "path": "/api/product-desc/n8n-generate", "description": "Trigger từ n8n để sinh mô tả thô trước khi public." },
{ "method": "POST", "path": "/api/product-desc/batch-generate", "description": ẩy danh sách ID vào RabbitMQ/Redis Queue để xử lý ngầm." }
]
},
{
"title": "5. Code Snippet Thực Tế (n8n API Hook)",
"type": "code",
"language": "python",
"code": "from fastapi import APIRouter\nfrom schemas.product import N8nDescRequest\n\n@router.post(\"/n8n-generate\")\nasync def n8n_generate_description(req: N8nDescRequest):\n \"\"\"Endpoint được gọi từ n8n Workflow khi có SP mới tải lên Google Sheets\"\"\"\n # 1. Map season tags cứng\n deterministic_tags = map_season_tags(req.season)\n \n # 2. Gọi AI sinh content\n prompt = build_ultra_prompt(req.name, req.image_urls, deterministic_tags)\n ai_response = await llm.ainvoke(prompt)\n \n # 3. Trả về cho n8n để nó tự động điền vào Sheets\n return {\"status\": \"success\", \"data\": ai_response.content}"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi Timeout khi Batch Generate:</b><br>Khi nhấn Generate cho 50 SP cùng lúc, nếu không dùng Background Task (RabbitMQ/Celery) mà bắt API chờ, thì Frontend sẽ bị 504 Gateway Timeout. Giải pháp là API chỉ trả về `job_id`, và Worker chạy ngầm.<br><br><b>🚫 Lỗi Rate Limit (429) với Gemini API:</b><br>Gemini API có giới hạn RPM (Requests Per Minute) rất gắt. Trong file <code>product-desc.js</code> và Backend phải cài đặt Retry <b>Exponential Backoff</b> với thư viện <code>tenacity</code>."
}
]
}
{
"title": "Fashion Matching Engine",
"description": "Khám phá thuật toán Stylist AI đằng sau các gợi ý phối đồ (Outfits), dựa trên hệ thống quy tắc thời trang và ma trận phối màu.",
"description": "Khám phá thuật toán đằng sau các gợi ý phối đồ (Outfits), dựa trên hệ thống quy tắc cứng (Rule-based) kết hợp với LLM reasoning.",
"diagram": "diagrams/fashion-matches.svg",
"sections": [
{
"title": "Cơ chế hoạt động",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "Engine phối đồ hoạt động theo 5 bước chính:<br>1. <b>Phân tích Anchor:</b> Xác định loại sản phẩm, giới tính và màu sắc của item gốc.<br>2. <b>Tra cứu Quy tắc:</b> Tìm các công thức phối hợp trong bảng <code>chatbot_fashion_rules</code> (ví dụ: Áo Polo nam hợp với Quần Khaki hoặc Quần Jean).<br>3. <b>Ma trận Màu sắc:</b> Tính toán synergy score giữa màu anchor và màu các item ứng viên dựa trên ma trận 16x16 màu.<br>4. <b>Lọc Điều kiện:</b> Loại bỏ các item hết hàng hoặc lệch mùa.<br>5. <b>Xếp hạng:</b> Trả về Top 3 item tốt nhất cho mỗi vị trí (Top, Bottom, Outerwear, Accessory)."
"content": "Fashion Match Engine là thuật toán gợi ý đồ bộ (Outfit) cho 1 sản phẩm gốc (Anchor Item). Mục tiêu là đảm bảo AI luôn gợi ý ĐÚNG QUY TẮC (Ví dụ: Không bao giờ gợi ý Quần Ống Rộng đi với Áo Oversize cho người thấp). Bằng cách kết hợp giữa SQLite Rules (Logic cứng) và LLM (Ngôn ngữ mềm), hệ thống ngăn chặn triệt để tình trạng AI \"ảo giác\" ra set đồ thảm họa."
},
{
"title": "Ma trận Phối màu (Color Synergy)",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "Màu sắc được chia thành 3 nhóm: <b>Neutral</b> (Trắng, Đen, Be, Xám), <b>Light</b> (Hồng, Vàng, Xanh lam), và <b>Dark</b> (Đỏ, Cam, Xanh navy).<br>- Neutral + Any: Điểm cao (An toàn).<br>- Light + Light hoặc Dark + Dark: Điểm trung bình (Cần cân nhắc).<br>- Tương phản mạnh: Điểm cao (Cá tính)."
"content": "Quy trình phối đồ đi qua 4 bước khắt khe:<br><br><b>1. Phân tích Anchor:</b> Trích xuất Category, Giới tính, Dáng áo (Form), và Màu sắc của sản phẩm khách đang xem.<br><b>2. Truy vấn Rule Base:</b> Query bảng <code>chatbot_fashion_rules</code> (SQLite) để lấy các \"Công thức\" hợp lệ. (Ví dụ: <code>Áo Polo -> Quần Khaki, Quần Jean</code>).<br><b>3. Chấm điểm Màu sắc (Color Matrix):</b> Điểm <code>synergy_score</code> được tính từ ma trận 16x16 màu. (Ví dụ: Trắng + Đen = 10đ, Cam + Xanh lá = 3đ).<br><b>4. Lọc Tồn Kho & Xếp hạng:</b> Gọi sang module Product Stock để loại bỏ những item đã hết hàng, sau đó lấy Top 3 cho từng Slot (Top, Bottom, Outerwear, Accessory)."
},
{
"title": "API Phối đồ",
"title": "3. Core Logic: Khung 4-Slot Framework",
"type": "text",
"content": "Để tránh việc AI gợi ý lung tung, hệ thống ép toàn bộ Outfit vào một khung 4-Slot cố định:<br>- <b>Top:</b> Áo (Phông, Polo, Sơ mi).<br>- <b>Bottom:</b> Quần/Chân váy.<br>- <b>Set/One-piece:</b> Đồ liền (Váy liền, Jumpsuit).<br>- <b>Accessories:</b> Phụ kiện (Mũ, Tất, Thắt lưng).<br>Ví dụ: Nếu khách đang xem Áo (Top), AI bắt buộc phải tìm Quần (Bottom) và Phụ kiện. AI không được phép gợi ý thêm 1 cái Áo khác trừ khi đó là Áo Khoác (Outerwear)."
},
{
"title": "4. API Phối đồ",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/fashion-matches/{code}", "description": "Lấy danh sách các item phối cùng đã tính toán sẵn." },
{ "method": "POST", "path": "/api/fashion-matches/outfit-suggest", "description": "Gợi ý outfit đầy đủ theo dịp (Occasion) cụ thể." },
{ "method": "GET", "path": "/api/fashion-matches/rules/config", "description": "Lấy cấu hình các quy tắc thời trang." }
{ "method": "GET", "path": "/api/fashion-matches/{code}", "description": "Lấy danh sách các item phối cùng đã tính toán sẵn (Cache)." },
{ "method": "POST", "path": "/api/fashion-matches/outfit-suggest", "description": "AI Gen Outfit thời gian thực theo sự kiện (Ví dụ: Đi tiệc)." },
{ "method": "GET", "path": "/api/fashion-matches/rules/config", "description": "Admin API: Tải bộ quy tắc phối đồ từ DB." }
]
},
{
"title": "5. Code Snippet Thực Tế (match_algo.py)",
"type": "code",
"language": "python",
"code": "def calculate_color_synergy(anchor_color: str, candidate_color: str) -> int:\n \"\"\"Tính điểm hợp màu dựa trên ma trận cứng thay vì hỏi LLM\"\"\"\n # Các màu an toàn (Neutral) luôn hợp mọi thứ\n neutrals = ['Trắng', 'Đen', 'Be', 'Xám', 'Ghi']\n \n if anchor_color in neutrals or candidate_color in neutrals:\n return 8 # Safe match\n \n # Quy tắc tương phản (Tonal/Contrast)\n if is_complementary(anchor_color, candidate_color):\n return 10 # High fashion\n \n if is_clashing(anchor_color, candidate_color):\n return 2 # Bad match\n \n return 5"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi AI Gợi ý đồ Hết Hàng:</b><br>Thuật toán Match có thể tìm ra Outfit rất đẹp nhưng tỷ lệ Click sẽ = 0 nếu hàng hết size. LUÔN phải bọc vòng lặp <code>filter(is_in_stock)</code> trước khi nhét vào Prompt cho AI viết văn.<br><br><b>🚫 Lỗi Quá tải Phụ kiện (Over-accessorizing):</b><br>Nếu không ép Slot Framework, LLM có xu hướng khuyên khách hàng mặc 1 cái váy kèm theo mũ, kính, vòng cổ, tất, thắt lưng cùng lúc. Quy tắc cứng: Tối đa 2 item trong slot <code>Accessories</code>."
}
]
}
......@@ -4,23 +4,39 @@
"diagram": "diagrams/stock-cache.svg",
"sections": [
{
"title": "Product Performance (StarRocks)",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "Dashboard hiệu năng sử dụng <b>StarRocks</b> làm OLAP engine để tính toán các chỉ số Aggregate (Tổng bán, Tồn kho, Giá trung bình) trên hàng triệu bản ghi Magento trong thời gian thực.<br>Dữ liệu được nhóm theo <code>internal_ref_code</code> để hiển thị cái nhìn tổng quan theo mẫu mã thay vì từng màu/size riêng lẻ."
"content": "Đây là module quản trị Catalog và Tồn kho (Inventory). Hệ thống kết hợp giữa sức mạnh OLAP của <b>StarRocks</b> để tính toán KPIs siêu tốc trên hàng triệu rows, và Redis/httpx để lấy data tồn kho Realtime từ ERP mà không gây nghẽn mạng."
},
{
"title": "Cơ chế Check Tồn kho Realtime",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "Vì API tồn kho của Canifa chỉ nhận tối đa 200 SKU/lần, hệ thống triển khai cơ chế <b>Parallel Fetching</b>:<br>1. <b>Expand:</b> Chuyển đổi mã gốc sang toàn bộ bộ SKU (Màu-Size) hợp lệ.<br>2. <b>Chunking:</b> Chia danh sách thành các nhóm nhỏ (50 item/nhóm).<br>3. <b>Async Call:</b> Sử dụng <code>httpx.AsyncClient</code> để gọi đồng thời nhiều request, giảm latency tổng thể xuống dưới 2 giây."
"content": "Vì API tồn kho của Canifa ERP có giới hạn (Rate Limit) và chỉ nhận tối đa 200 SKU/lần, hệ thống phải triển khai cơ chế <b>Parallel Chunking Fetch</b>:<br><br><b>1. Expand (Bùng nổ SKU):</b> Khách hàng tra cứu mã Gốc (Ví dụ: `8TS24A001`). Hệ thống phải nổ mã này ra thành 15 SKU con (Đỏ-S, Đỏ-M, Xanh-L...).<br><b>2. Chunking (Chia nhỏ):</b> Cắt mảng 1000 SKUs thành các chunks nhỏ (mỗi chunk 50 SKU).<br><b>3. Async Scatter (Bắn đồng thời):</b> Dùng <code>httpx.AsyncClient</code> với <code>asyncio.gather()</code> bắn song song 20 requests lên ERP ERP.<br><b>4. Gather & Cache:</b> Gộp kết quả trả về, update vào Redis Cache với TTL 15 phút để các lượt check sau không phải gọi lại ERP."
},
{
"title": "API Hiệu năng & Tồn kho",
"title": "3. Core Logic: Product Performance (StarRocks)",
"type": "text",
"content": "Thay vì truy vấn Postgres chậm chạp, Dashboard sử dụng <b>StarRocks</b> làm OLAP engine để GROUP BY và SUM() doanh thu, số bán, tồn kho. Dữ liệu được nhóm theo <code>internal_ref_code</code> (Mã mẫu) để hiển thị cái nhìn tổng quan thay vì liệt kê từng màu/size lắt nhắt."
},
{
"title": "4. API Hiệu năng & Tồn kho",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/products/overview", "description": "Lấy các chỉ số KPI tổng quát của toàn bộ kho hàng." },
{ "method": "GET", "path": "/api/products/list", "description": "Danh sách sản phẩm kèm hiệu số bán chạy và tìm kiếm thông minh." },
{ "method": "POST", "path": "/api/stock/check", "description": "Kiểm tra tồn kho realtime cho danh sách sản phẩm." }
{ "method": "POST", "path": "/api/stock/check", "description": "Kiểm tra tồn kho realtime cho danh sách sản phẩm qua ERP." }
]
},
{
"title": "5. Code Snippet Thực Tế (Parallel Fetching)",
"type": "code",
"language": "python",
"code": "import asyncio\nimport httpx\n\nasync def fetch_stock_parallel(sku_list: list[str]):\n \"\"\"Chia nhỏ list SKU và gọi API ERP song song\"\"\"\n chunk_size = 50\n chunks = [sku_list[i:i + chunk_size] for i in range(0, len(sku_list), chunk_size)]\n \n async with httpx.AsyncClient() as client:\n tasks = [\n client.post(\"https://erp.canifa.com/api/stock\", json={\"skus\": chunk})\n for chunk in chunks\n ]\n \n # Chạy đồng thời tất cả các HTTP requests\n responses = await asyncio.gather(*tasks, return_exceptions=True)\n \n final_stock = {}\n for res in responses:\n if not isinstance(res, Exception) and res.status_code == 200:\n final_stock.update(res.json().get(\"data\", {}))\n return final_stock"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi Too Many Open Files (Socket Exhaustion):</b><br>Khi số lượng Task sinh ra quá lớn, HTTPX mở quá nhiều TCP connections dẫn đến lỗi OS. Giải pháp: Phải dùng <code>asyncio.Semaphore(10)</code> để giới hạn tối đa 10 requests chạy song song cùng lúc.<br><br><b>🚫 Cache Stampede (Bão Cache):</b><br>Khi Cache hết hạn (TTL expire), nếu có 100 users cùng check kho, cả 100 người sẽ cùng gọi lên ERP khiến server sập. Cần dùng cơ chế <b>Lock (Mutex)</b> để chỉ cho phép 1 user gọi ERP, 99 người còn lại chờ kết quả trả về."
}
]
}
{
"title": "AI Image Search",
"description": "Hướng dẫn cách hệ thống tìm kiếm sản phẩm thông qua hình ảnh bằng cách kết hợp Vision LLM và Vector Similarity Search.",
"description": "Chi tiết luồng tìm kiếm hình ảnh kết hợp giữa Vision LLM (để bóc tách thuộc tính) và Vector Similarity Search trên StarRocks.",
"diagram": "diagrams/image-search.svg",
"sections": [
{
"title": "Quy trình Tìm kiếm Đa phương thức",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "1. <b>Vision Analysis:</b> Khi người dùng tải ảnh lên, Vision LLM (Llama-4 Scout hoặc GPT-4o) sẽ phân tích các thuộc tính thị giác như: loại sản phẩm, màu sắc, họa tiết, chất liệu vải.<br>2. <b>Embedding Generation:</b> Mô tả văn bản từ bước 1 được chuyển thành vector đặc trưng (Embedding).<br>3. <b>StarRocks Vector Search:</b> Thực hiện tìm kiếm láng giềng gần nhất (ANN) trên bảng <code>magento_product_dimension_with_text_embedding</code> để tìm ra các sản phẩm có độ tương đồng cao nhất về mặt ngữ nghĩa.<br>4. <b>Reranking:</b> Stylist Agent sẽ lọc lại danh sách dựa trên giới tính và độ tuổi của khách hàng trước khi trả về kết quả."
"content": "Image Search cho phép khách hàng up 1 tấm ảnh (Ví dụ: Ảnh người mẫu mặc váy hoa nhí) để tìm các sản phẩm tương tự trong kho Canifa. Điểm khác biệt là hệ thống không dùng thuật toán Image-to-Image thuần túy (rất đắt và chậm) mà dùng kiến trúc <b>Image-to-Text-to-Vector</b>."
},
{
"title": "Kỹ thuật tối ưu",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "Hệ thống sử dụng hàm <code>approx_cosine_similarity</code> của StarRocks để đảm bảo tốc độ phản hồi dưới 500ms cho hàng chục ngàn sản phẩm."
"content": "Quy trình tìm kiếm bằng ảnh tốn chưa tới 1.5 giây, qua 4 bước:<br><br><b>1. Vision Analysis:</b> Khi User gửi ảnh Base64 lên, API gọi qua Llama-4-Scout (hoặc GPT-4o) để bóc tách các đặc điểm thị giác (Áo sơ mi tay ngắn, kẻ sọc xanh, cổ đức...).<br><b>2. Text-to-Embedding:</b> Dịch chuỗi text mô tả trên thành một Vector toán học (Array float32) bằng model <code>text-embedding-3-small</code> (1536 chiều).<br><b>3. Vector Search (HNSW):</b> Quét Vector vừa tạo với hàng ngàn Vector sản phẩm trong <code>StarRocks</code> bằng thuật toán xấp xỉ <code>approx_cosine_similarity</code>.<br><b>4. LLM Reranking (Lọc lại):</b> Lấy top 10 kết quả từ DB đưa lại cho Agent để nó đối chiếu giá trị tồn kho, loại bỏ những SP sai giới tính/tuổi trước khi trả về Frontend."
},
{
"title": "API Image Search",
"title": "3. Core Logic: StarRocks Vector Index",
"type": "text",
"content": "Để StarRocks có thể quét Vector siêu tốc, bảng <code>magento_product_dimension</code> được thiết lập <b>HNSW Index</b> (Hierarchical Navigable Small World). Thuật toán này không tính khoảng cách Euclidean của tất cả bản ghi (Full Scan), mà nhảy qua các \"trạm trung chuyển\" (Graph nodes), giúp giảm độ trễ từ 3 giây xuống 200ms."
},
{
"title": "4. API Image Search",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/image-search/chat", "description": "Gửi ảnh (base64) và text query để tìm sản phẩm tương đương." }
{ "method": "POST", "path": "/api/image-search/chat", "description": "Gửi ảnh (Base64) kèm Text Query ('Tìm cho em cái áo giống thế này')." },
{ "method": "GET", "path": "/api/image-search/config", "description": "Lấy cấu hình Embedding Model đang dùng (OpenAI/Cohere)." }
]
},
{
"title": "5. Code Snippet Thực Tế (Vector Query)",
"type": "code",
"language": "python",
"code": "async def search_similar_products(query_vector: list[float], limit: int = 5):\n \"\"\"Tìm Top K sản phẩm gần giống nhất bằng Cosine Similarity trên StarRocks\"\"\"\n # Format vector thành chuỗi mảng SQL\n vector_str = \"[\" + \",\".join(map(str, query_vector)) + \"]\"\n \n query = f\"\"\"\n SELECT \n internal_ref_code, \n product_name,\n approx_cosine_similarity(embedding_vector, {vector_str}) AS score\n FROM magento_product_dimension_with_text_embedding\n WHERE approx_cosine_similarity(embedding_vector, {vector_str}) > 0.75\n ORDER BY score DESC\n LIMIT {limit}\n \"\"\"\n return await db.execute(query)"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi Base64 Quá Lớn (Payload Too Large):</b><br>Nếu User chụp ảnh bằng iPhone 15 (10MB) và convert Base64, chuỗi sẽ dài hàng triệu ký tự khiến FastAPI quăng lỗi <code>413 Payload Too Large</code>. Phải có Middleware/Frontend tự động nén ảnh xuống dưới 1024x1024 (JPEG) trước khi gửi.<br><br><b>🚫 Lỗi Dimension Mismatch:</b><br>Nếu trong DB đang dùng model text-embedding-ada-002 (1536 chiều), mà API lại dùng nomic-embed-text (768 chiều), StarRocks sẽ crash ngay khi gọi hàm similarity. Luôn phải đồng nhất model Embedding khi tạo DB và khi truy vấn."
}
]
}
{
"title": "Text-to-SQL & Data Analyst",
"description": "Tìm hiểu cách hệ thống chuyển đổi ngôn ngữ tự nhiên thành các câu lệnh SQL phức tạp để phân tích dữ liệu bán hàng trên StarRocks.",
"description": "Chi tiết luồng kiến trúc chuyển đổi ngôn ngữ tự nhiên thành câu lệnh SQL phức tạp, thực thi trên StarRocks và tổng hợp Insight bán hàng.",
"diagram": "diagrams/text-to-sql.svg",
"sections": [
{
"title": "Cơ chế Text-to-SQL",
"title": "1. Tổng quan (Overview)",
"type": "text",
"content": "Module sử dụng Agent có khả năng tự nhận diện Schema (Schema Awareness). Thay vì nhét toàn bộ DDL vào prompt, Agent thực hiện qua các bước:<br>1. <b>Table Selection:</b> Chọn các bảng liên quan đến câu hỏi (Sản phẩm, Đơn hàng, Tồn kho).<br>2. <b>SQL Generation:</b> Sinh câu lệnh SQL dialect StarRocks (tương thích MySQL).<br>3. <b>Execution:</b> Chạy query trên database StarRocks và nhận kết quả JSON.<br>4. <b>Self-Correction:</b> Nếu SQL lỗi, Agent sẽ đọc thông báo lỗi và thử sinh lại tối đa 3 lần."
"content": "Module Text-to-SQL cho phép Ban Giám Đốc hoặc Data Analyst chat trực tiếp với Database bằng ngôn ngữ tự nhiên. Điểm đặc biệt của kiến trúc này là <b>Schema Awareness</b> và <b>Self-Correction</b>. Thay vì nhồi nhét hàng trăm bảng vào một prompt (gây quá tải token), AI tự động tìm kiếm bảng phù hợp và tự động sửa lỗi SQL nếu query bị crash."
},
{
"title": "AI Data Analyst",
"title": "2. Kiến trúc & Luồng dữ liệu (Data Flow)",
"type": "text",
"content": "Sau khi có dữ liệu thô, Analyst Node sẽ:<br>- <b>Summarization:</b> Tóm tắt các con số quan trọng (Tổng doanh thu, % tăng trưởng).<br>- <b>Insight Extraction:</b> Tìm ra các điểm bất thường hoặc xu hướng (ví dụ: 'Áo phông nam đang bán chậm ở miền Nam').<br>- <b>Visualization Suggestion:</b> Đề xuất loại biểu đồ phù hợp (Cột, Tròn, Đường) để frontend hiển thị."
"content": "Quá trình sinh SQL đi qua 4 Node chính trong LangGraph:<br><br><b>1. Table Selection Node:</b> AI phân tích câu hỏi (VD: <i>\"Top 5 mẫu áo bán chạy nhất tháng trước\"</i>) và quyết định gọi ra Schema của bảng <code>orders</code>, <code>order_items</code> và <code>products</code>.<br><b>2. SQL Generation Node:</b> AI sinh ra câu SQL theo Dialect của StarRocks (tương thích cú pháp MySQL). Lớp Guardrails được cài vào đây để chặn câu lệnh <code>DROP</code>, <code>DELETE</code>, <code>UPDATE</code>.<br><b>3. Execution Node:</b> Hệ thống chạy câu SQL trên Database thông qua Read-Only User. Nếu thành công, trả về JSON data. Nếu lỗi (VD: sai tên cột), chuyển sang bước Self-Correction.<br><b>4. Self-Correction Node:</b> AI đọc thông báo lỗi từ Database (Ví dụ: <i>Column 'revenue' not found</i>), tự sửa câu SQL và thử chạy lại tối đa 3 lần."
},
{
"title": "API SQL & Insights",
"title": "3. AI Data Analyst (Insight Extraction)",
"type": "text",
"content": "Sau khi Data SQL được trả về dưới dạng list of dicts, thay vì dump nguyên JSON cho user, dữ liệu sẽ được đẩy qua <b>Analyst Node</b>. Nhiệm vụ của Node này:<br>- <b>Summarization:</b> Tóm tắt số liệu thành một đoạn chắt lọc (Ví dụ: \"Doanh thu tháng 10 đạt 500 củ, tăng 20%\").<br>- <b>Insight Mining:</b> Bóc tách các xu hướng ẩn (Ví dụ: \"Màu xanh navy đang chiếm 60% doanh số\").<br>- <b>Visualization Suggestion:</b> Gợi ý UI render biểu đồ (Bar, Pie, Line) tùy vào đặc thù dữ liệu."
},
{
"title": "4. API SQL & Insights",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/sql/chat", "description": "Chat với dữ liệu để lấy insight và biểu đồ." },
{ "method": "POST", "path": "/api/sql/generate", "description": "Chỉ sinh code SQL từ ngôn ngữ tự nhiên (cho Admin)." },
{ "method": "GET", "path": "/api/sql/user-insight/{id}", "description": "Lấy hồ sơ insight chi tiết của 1 khách hàng cụ thể." }
{ "method": "POST", "path": "/api/sql/chat", "description": "End-to-End Chat: Sinh SQL, Execute, Tóm tắt Insight." },
{ "method": "POST", "path": "/api/sql/generate", "description": "Chỉ sinh SQL thô (Phục vụ tool cho Data Engineer)." },
{ "method": "GET", "path": "/api/sql/user-insight/{id}", "description": "Trích xuất hồ sơ phân tích hành vi của 1 user cụ thể." }
]
},
{
"title": "5. Code Snippet Thực Tế (Guardrails)",
"type": "code",
"language": "python",
"code": "import re\nfrom fastapi import HTTPException\n\ndef validate_generated_sql(sql_query: str) -> str:\n \"\"\"Ngăn chặn AI sinh câu lệnh phá hoại Database\"\"\"\n sql_upper = sql_query.upper()\n \n # Danh sách từ khóa cấm kỵ (DML/DDL)\n forbidden_keywords = [\"DROP\", \"DELETE\", \"UPDATE\", \"INSERT\", \"ALTER\", \"TRUNCATE\", \"GRANT\"]\n \n for keyword in forbidden_keywords:\n # Regex check keyword tồn tại như một từ độc lập\n if re.search(fr'\\b{keyword}\\b', sql_upper):\n logger.error(f\"🚨 SECURITY ALERT: AI attempted {keyword} operation\")\n raise HTTPException(status_code=403, detail=\"Query contains forbidden operations\")\n \n return sql_query"
},
{
"title": "6. Troubleshooting & Gotchas",
"type": "text",
"content": "<b>🚫 Lỗi Token Limit Exceeded:</b><br>Khi query trả về hàng chục ngàn dòng dữ liệu (Ví dụ: <i>\"Lấy tất cả đơn hàng năm 2025\"</i>), Analyst Node sẽ quá tải Token. Cần luôn tiêm <code>LIMIT 50</code> vào Prompt hoặc yêu cầu AI sinh SQL kiểu Aggregation (GROUP BY) thay vì lấy raw data.<br><br><b>🚫 Vấn đề ảo giác Cột (Hallucinated Columns):</b><br>AI có thể tự bịa ra cột <code>created_at</code> trong khi bảng chỉ có <code>order_date</code>. Giải pháp: Ở bước Table Selection, phải inject chính xác DDL schema (Tên cột, Kiểu dữ liệu, Comment) vào prompt của LLM."
}
]
}
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Authentication & Routing Flow</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Authentication &amp; Routing Flow</text>
<!-- Flow -->
<rect x="380" y="80" width="200" height="50" rx="25" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......
......@@ -11,7 +11,7 @@
</filter>
</defs>
<rect width="960" height="700" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Chatbot Pipeline & LangGraph Agent Architecture</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Chatbot Pipeline &amp; LangGraph Agent Architecture</text>
<!-- Input -->
<rect x="50" y="100" width="120" height="60" rx="30" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......
......@@ -12,7 +12,7 @@
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Database Layer & SQL Translation Flow</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Database Layer &amp; SQL Translation Flow</text>
<!-- Application Layer -->
<rect x="100" y="100" width="300" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
......@@ -23,7 +23,7 @@
<text x="710" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Sync Helper Classes</text>
<text x="710" y="155" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">UltraDescDB / Models</text>
<!-- Middle Layer: Wrapper & Interceptor -->
<!-- Middle Layer: Wrapper &amp; Interceptor -->
<rect x="560" y="240" width="300" height="60" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="710" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Pool Wrapper</text>
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">FAQ Manager & BM25 Hybrid Search Flow</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">FAQ Manager &amp; BM25 Hybrid Search Flow</text>
<!-- Input -->
<rect x="50" y="80" width="150" height="60" rx="30" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......
......@@ -35,7 +35,7 @@
<text x="390" y="390" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Constraint Filtering</text>
<rect x="300" y="430" width="180" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="390" y="460" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Ranking & Scoring</text>
<text x="390" y="460" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Ranking &amp; Scoring</text>
<!-- Knowledge Source -->
<rect x="580" y="150" width="180" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Feedback Classification & Monitoring Pipeline</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Feedback Classification &amp; Monitoring Pipeline</text>
<!-- Flow -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......@@ -25,7 +25,7 @@
<rect x="280" y="220" width="200" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="255" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">GPT-4o-mini Classifier</text>
<text x="380" y="275" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">Categorize & Summarize</text>
<text x="380" y="275" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">Categorize &amp; Summarize</text>
<path d="M480,260 L580,260" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Session & History Merge Flow</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Session &amp; History Merge Flow</text>
<!-- Guest Phase -->
<rect x="50" y="100" width="200" height="80" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Prompt Management & Cache Refresh Flow</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Prompt Management &amp; Cache Refresh Flow</text>
<!-- Admin -->
<rect x="50" y="100" width="120" height="50" rx="4" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">SKU & Store Search Pipelines</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">SKU &amp; Store Search Pipelines</text>
<!-- SKU Search -->
<rect x="50" y="100" width="400" height="350" rx="12" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1" />
......@@ -43,6 +43,6 @@
<path d="M710,310 L710,350" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="610" y="350" width="200" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" />
<text x="710" y="385" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Store List & Schedule</text>
<text x="710" y="385" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Store List &amp; Schedule</text>
</svg>
\ No newline at end of file
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Social Content Creation & Approval Workflow</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Social Content Creation &amp; Approval Workflow</text>
<!-- Flow -->
<rect x="50" y="100" width="180" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Testing & AI Simulation Suite</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Testing &amp; AI Simulation Suite</text>
<!-- Platform Core -->
<rect x="330" y="380" width="300" height="80" rx="12" fill="#f8fafc" stroke="#1e293b" stroke-width="2" />
......
......@@ -8,7 +8,7 @@
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Text-to-SQL & AI Data Analyst Pipeline</text>
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Text-to-SQL &amp; AI Data Analyst Pipeline</text>
<!-- Flow -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
......@@ -36,7 +36,7 @@
<!-- Analyst -->
<rect x="280" y="380" width="200" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="415" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">AI Analyst Node</text>
<text x="380" y="435" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">Insights & Charts</text>
<text x="380" y="435" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">Insights &amp; Charts</text>
<!-- Dashboard -->
<path d="M480,420 L600,420" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
......
......@@ -34,42 +34,38 @@ def _next_groq_key() -> str:
return key
# ═══ Tags Allowed (Plain, No Prefix) ═══
# Color tones + specific colors + seasons + occasions + styles + fits
TAGS_ALLOWED = [
# Color tones
"trung tính", "sáng", "đậm",
# Specific colors
"đen", "trắng", "xám", "nâu", "vàng", "hồng", "xanh da trời", "xanh than", "tím", "đỏ", "cam", "xanh lá",
# Seasons
"mùa hè", "mùa đông", "mùa xuân", "mùa thu",
# Occasions
"đi học", "đi chơi", "đi tiệc", "dạo phố", "mặc nhà", "đi ngủ",
"thể thao", "đi dã ngoại", "đi biển", "đi làm", "giữ ấm", "thoáng mát",
# Styles
"thanh lịch", "năng động", "basic", "cá tính", "dễ thương", "trẻ trung", "tối giản", "smart casual",
# Fit
"oversize", "slim", "regular", "wide leg", "cropped", "relaxed"
]
TAG_GENERATION_PROMPT = """Bạn là chuyên gia gán tag thời trang cho sản phẩm Canifa.
NHIỆM VỤ:
Đọc mô tả sản phẩm bên dưới và chọn TỐI ĐA 6 tags phù hợp nhất từ DANH SÁCH TAGS HỢP LỆ.
DANH SÁCH TAGS HỢP LỆ:
--- TRỤC 1 — Hoàn cảnh & Dịp (occ:) ---
occ:di_lam, occ:di_choi, occ:di_tiec, occ:di_hoc,
occ:mac_nha, occ:the_thao, occ:di_bien, occ:du_lich,
occ:da_ngoai, occ:di_ngu
--- TRỤC 2a — Thời tiết (wthr:) ---
wthr:mua_he, wthr:mua_dong, wthr:giao_mua,
wthr:troi_mua, wthr:troi_nang
--- TRỤC 2b — Tính năng vải (func:) ---
func:thoang_mat, func:giu_am, func:tham_hut,
func:nhanh_kho, func:chong_uv, func:can_gio
--- TRỤC 3 — Phong cách & Vibe (style:) ---
style:thanh_lich, style:nang_dong, style:basic,
style:ca_tinh, style:de_thuong, style:tre_trung,
style:toi_gian, style:smart_casual
Đọc mô tả sản phẩm và chọn TỐI ĐA 6 tags phù hợp nhất từ DANH SÁCH TAGS HỢP LỆ.
--- TRỤC 4 — Dáng & Kiểu cắt (fit:) ---
fit:oversize, fit:slim, fit:regular,
fit:wide_leg, fit:cropped, fit:relaxed
DANH SÁCH TAGS HỢP LỄ:
""" + "\n".join(TAGS_ALLOWED) + """
QUY TẮC BẮT BUỘC:
1. Chỉ được dùng tags trong danh sách trên — KHÔNG được bịa tag mới
QUY TẮC:
1. CHỈ dùng tags trong danh sách trên — KHÔNG bịa tag mới
2. Chọn tối đa 6 tags, tối thiểu 2 tags
3. Cố gắng có ít nhất 1 tag occ: + 1 tag style: hoặc fit: trong kết quả
4. fit: và style: là 2 trục KHÁC NHAU — fit:oversize ≠ style:ca_tinh
5. Nếu mô tả không đủ thông tin để gán 1 trục nào đó, bỏ qua trục đó, KHÔNG đoán mò
6. Chỉ trả về một list Python duy nhất, KHÔNG giải thích, KHÔNG markdown
7. CẤM BẮT BUỘC: KHÔNG gán occ:di_tiec cho áo phông (thun), đồ trẻ em, đồ in hình hoạt hình (Stitch, Mickey...). Chỉ gán occ:di_tiec cho trang phục sang trọng (váy dạ hội, sơ mi dự tiệc, áo blazer, quần âu).
3. Có thể chọn từ bất kỳ nhóm nào (màu sắc, mùa, dịp, phong cách, dáng)
4. Chỉ trả về một list Python (ví dụ: ["đi làm", "thoáng mát", "mùa hè"])
5. KHÔNG giải thích, KHÔNG markdown
MÔ TẢ SẢN PHẨM:
{product_description}
......@@ -155,9 +151,18 @@ async def handle_tags_job(redis, job_data, client, conn):
err_count += 1
return
name, p_line, clean_desc, current_tags = row
if current_tags and len(current_tags) > 0:
done_count += 1
return
# Parse current tags (may be JSON string or list)
old_tags = []
if current_tags:
if isinstance(current_tags, str):
try: old_tags = json.loads(current_tags)
except: old_tags = []
elif isinstance(current_tags, list):
old_tags = current_tags
# Ensure list
if not isinstance(old_tags, list):
old_tags = []
full_name = name or p_line or ''
tags = None
......@@ -166,11 +171,17 @@ async def handle_tags_job(redis, job_data, client, conn):
if not tags: await asyncio.sleep(3)
if tags and isinstance(tags, list):
valid_prefixes = ("occ:", "wthr:", "func:", "style:", "fit:")
clean_tags = [t for t in tags if str(t).startswith(valid_prefixes)]
# Filter: only keep tags in TAGS_ALLOWED (no prefix check)
clean_tags = [t for t in tags if t in TAGS_ALLOWED]
# Merge: keep old tags, add new ones not already present
merged_tags = old_tags[:]
for t in clean_tags:
if t not in merged_tags:
merged_tags.append(t)
cur.execute(
f"UPDATE {PG_TABLE} SET tags = %s::jsonb, updated_at = NOW() WHERE internal_ref_code = %s",
(json.dumps(clean_tags, ensure_ascii=False), code)
(json.dumps(merged_tags, ensure_ascii=False), code)
)
conn.commit()
else: err_count += 1
......
# 🚀 DOING: Architecture & Bottleneck Fixes
## 📌 Context
- **Idea:** Backend Architecture Audit (2026-04-29)
- **Status:** ✅ Done
## 🎯 Goal
Fix the 7 identified bottlenecks and critical issues in the FastAPI backend to ensure production-grade stability, prevent deadlocks, and future-proof the application.
## ⚠️ Active Trade-offs
- None. These are strict improvements without functional regressions.
## 🛠 Impact & Files Touched
| File | Symbol | Change | Blast Radius |
|------|--------|--------|--------------|
| `config.py` | `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` | DELETE | Low (Duplicate block removed) |
| `server.py` | `startup_event`, `shutdown_event` | MODIFY | Medium (App lifecycle updated to `lifespan`) |
| `starrocks_connection.py` | `_pool_lock`, `get_pool` | MODIFY | Medium (Thread/Async safety for DB pool) |
| `pool_wrapper.py` | `get_pooled_connection_compat` | MODIFY | Low (Added backoff to retry) |
**Risk:** Medium (Touching startup lifecycle and DB connections. Will verify via server restart).
## 📋 Execution Checklist
### Phase 1: P0 Critical Fixes (~10m)
- [x] Task 1.1: Fix duplicate LANGFUSE keys in `config.py`.
- [x] Task 1.2: Migrate `@app.on_event` to `lifespan` in `server.py` (Includes moving `start_publish_engine`).
- [x] Task 1.3: Fix `asyncio.Lock()` initialization in `starrocks_connection.py` to be inside the async context.
### Phase 2: P1 Performance Fixes (~5m)
- [x] Task 2.1: Add exponential backoff to connection retries in `pool_wrapper.py`.
- [x] Task 2.2: Reduce `STARROCKS_POOL_MAXSIZE` default from 80 to 20 in `starrocks_connection.py`.
### Phase 3: Cleanup (~5m)
- [x] Task 3.1: Remove unused root-level legacy databases (`123.db`).
## 🔄 Sub-Doings
_None yet_
## ✅ Completion Gate
- [x] All tasks [x] | [x] Sub-Doings resolved | [x] Smoke test passed | [x] Trade-offs confirmed
---
_Started: 2026-04-29 | Updated: 2026-04-29_
# 🚀 DOING: Cookbook Deep Dive - Nâng Cấp Chi Tiết Tận Răng
## 📌 Context
- **Idea:** `Nâng cấp Cookbook (Documentation) chi tiết, bài bản`
- **Report:** N/A (Directly approved via implementation_plan.md)
- **Status:** ✅ Done
## 🎯 Goal
Nâng cấp 9 tài liệu cốt lõi trong Cookbook (`01`, `01b`, `02`, `03`, `05`, `06`, `07`, `09`, `11`) từ dạng Overview cơ bản thành chuẩn "Deep Dive" (chi tiết luồng dữ liệu, giải thích logic, trích xuất mã nguồn thực tế và troubleshooting) nhằm làm tài liệu chuẩn mực cho Developer/BA.
## ⚠️ Active Trade-offs
- Skipping: Cập nhật các UI layout phức tạp trong frontend Cookbook — Lý do: Tập trung nâng cấp chất lượng nội dung JSON data trước.
- Skipping: Phase 3 (Hạ tầng & Công cụ) — Lý do: Ưu tiên làm Core Systems và Business Logic trước để tránh quá tải.
## 🛠 Impact & Files Touched
| File | Symbol | Change | Blast Radius |
|------|--------|--------|--------------|
| `static/cookbook/data/01-architecture.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/01b-database.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/02-chatbot-core.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/03-prompt-system.json`| JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/11-text-to-sql.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/05-ultra-description.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/06-fashion-matches.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/07-product-stock.json` | JSON format | MODIFY | 0 callers (Docs only) |
| `static/cookbook/data/09-image-search.json` | JSON format | MODIFY | 0 callers (Docs only) |
**Risk:** Low (Chỉ thay đổi data hiển thị của tài liệu nội bộ)
## 🔙 Rollback Plan
N/A
## 📋 Execution Checklist
### Phase 1: Core Systems (Hệ thống cốt lõi) (~30m)
- [x] Task 1.1: Viết lại `01-architecture.json` - Trình bày sâu về Micro-modular monolith, giao tiếp Interface & Gateway.
- [x] Task 1.2: Viết lại `01b-database.json` - Đi sâu vào `sqlite_mock.py` proxy pattern, Connection Pool (pool_wrapper.py) và cơ chế backoff.
- [x] Task 1.3: Viết lại `02-chatbot-core.json` - Khám phá LangGraph engine, Tool Node Execution logic và RAG context retrieval.
- [x] Task 1.4: Viết lại `03-prompt-system.json` - Trích code `llm_factory.py`, giải thích dynamic parameters prompt versioning.
- [x] Task 1.5: Viết lại `11-text-to-sql.json` - Kiến trúc Agentic Text-to-SQL, ảo hóa Schema, guardrails chặn Injection.
### Phase 2: Search & Fashion Logic (Nghiệp vụ Thời trang) (~30m)
- [x] Task 2.1: Viết lại `05-ultra-description.json` - Phân tích logic pipeline sinh mô tả, sync n8n với database.
- [x] Task 2.2: Viết lại `06-fashion-matches.json` - Code snippet từ thuật toán ghép đồ Top/Bottom/Accessories.
- [x] Task 2.3: Viết lại `07-product-stock.json` - Cơ chế Realtime Caching, phân tích file `cache.py`.
- [x] Task 2.4: Viết lại `09-image-search.json` - Quy trình embed ảnh qua CLIP model và tra cứu cosine similarity.
### Phase 3: Testing & Verification (~10m)
- [x] Task 3.1: Mở UI `localhost:5000/static/main.html?page=cookbook/cookbook.html` để verify format render.
- [x] Task 3.2: Check code blocks render qua thư viện Prism.js xem có bị vỡ layout không.
## 🔄 Sub-Doings
_None yet_
## ✅ Completion Gate
- [x] All tasks [x] | [x] Sub-Doings resolved | [x] Smoke test passed | [x] Trade-offs confirmed
---
_Started: 2026-04-29 | Updated: 2026-04-29_
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