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

feat(gender-rules): add gender_target to fashion_rules, seed 114 rules,...

feat(gender-rules): add gender_target to fashion_rules, seed 114 rules, /rules/view HTML endpoint, UI button
parent 014198f0
{
"permissions": {
"allow": [
"Bash(pip install *)",
"Bash(npm --version)",
"Bash(D:/cnf/Cuccu_sales_sass/backend/venv/Scripts/python.exe *)",
"Bash(/c/Users/25014271/cnf/Cuccu_sales_sass/backend/venv/Scripts/python.exe -c \"print\\('test'\\)\")",
"Bash(grep -oE '\\(?:FROM|INSERT INTO|UPDATE|DELETE FROM\\)\\\\s+[a-zA-Z_][a-zA-Z0-9_]*' app/services/**/*.py app/api/**/*.py)",
"Bash(ls -la *.py test*.py verify*.py)",
"Bash(python *)",
"Bash(python3 *)",
"Bash(where python *)",
"Bash(./venv/Scripts/python.exe *)",
"Bash(./venv/Scripts/pip.exe install *)",
"Bash(curl -s http://localhost:8000/health)",
"Bash(curl -s -X POST \"http://localhost:8000/api/workflows\" -H \"Content-Type: application/json\" --data \"{\\\\\"name\\\\\":\\\\\"Test\\\\\",\\\\\"chatbot_id\\\\\":\\\\\"018ed287-b7cd-483a-a9e2-2238b6558d4c\\\\\",\\\\\"user_id\\\\\":\\\\\"00000000-0000-0000-0000-000000000000\\\\\",\\\\\"nodes\\\\\":[{\\\\\"id\\\\\":\\\\\"start-1\\\\\",\\\\\"type\\\\\":\\\\\"start\\\\\",\\\\\"position\\\\\":{\\\\\"x\\\\\":100,\\\\\"y\\\\\":100},\\\\\"data\\\\\":{\\\\\"customer_id\\\\\":\\\\\"123\\\\\",\\\\\"message\\\\\":\\\\\"Hello\\\\\"}},{\\\\\"id\\\\\":\\\\\"agent-1\\\\\",\\\\\"type\\\\\":\\\\\"agent\\\\\",\\\\\"position\\\\\":{\\\\\"x\\\\\":300,\\\\\"y\\\\\":100},\\\\\"data\\\\\":{}},{\\\\\"id\\\\\":\\\\\"end-1\\\\\",\\\\\"type\\\\\":\\\\\"end\\\\\",\\\\\"position\\\\\":{\\\\\"x\\\\\":500,\\\\\"y\\\\\":100},\\\\\"data\\\\\":{}}],\\\\\"edges\\\\\":[{\\\\\"source\\\\\":\\\\\"start-1\\\\\",\\\\\"target\\\\\":\\\\\"agent-1\\\\\"},{\\\\\"source\\\\\":\\\\\"agent-1\\\\\",\\\\\"target\\\\\":\\\\\"end-1\\\\\"}]}\")",
"Bash(curl *)",
"Bash(taskkill /F /IM python.exe)",
"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)"
]
}
}
# CLAUDE.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
## 5. Project-Specific Workflow (The Plan System)
**Leverage the `plan` folder architecture (`ideas/`, `doing/`, `done/`).**
This project uses a rigorous Markdown-based Kanban system inside the `D:\cnf\chatbot-canifa-feedback\plan` directory.
You must strictly follow this lifecycle:
- **Task Phase:** Every pending task or objective must be represented as a markdown checklist file in `plan/tasks/`.
- **Doing Phase:** When you START working on any feature, you MUST FIRST move its markdown file from `plan/tasks/` to `plan/doings/`. Do not perform abstract coding; instead, follow the checklist items. Check them off `[x]` ONLY after verifying they work.
- **Completion Phase:** Once every item is verified and tests/success criteria pass, gracefully move the file to `plan/done/`. Never leave incomplete tasks in `plan/doings/` or un-tracked tasks floating around.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
...@@ -2003,7 +2003,7 @@ ...@@ -2003,7 +2003,7 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
......
...@@ -510,3 +510,73 @@ async def score_test(req: ScoreTestRequest): ...@@ -510,3 +510,73 @@ async def score_test(req: ScoreTestRequest):
except Exception as e: except Exception as e:
logger.error("[ScoreTest] error: %s", e) logger.error("[ScoreTest] error: %s", e)
return JSONResponse({"ok": False, "error": str(e)}, status_code=500) return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
# --- Rules Framework HTML View ---
@router.get("/rules/view")
async def rules_view():
from fastapi.responses import HTMLResponse
from collections import defaultdict
rows = []
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute("""
SELECT gender_target, anchor_category, occasion_tag, match_role, target_category, ai_reason
FROM dashboard_canifa.chatbot_fashion_rules
WHERE gender_target != 'all'
ORDER BY CASE gender_target
WHEN 'nu' THEN 1 WHEN 'nam' THEN 2 WHEN 'unisex' THEN 3
WHEN 'be_gai' THEN 4 WHEN 'be_trai' THEN 5 ELSE 6
END, occasion_tag, match_role, anchor_category
""")
rows = cur.fetchall()
cur.close()
except Exception as e:
logger.error("[RulesView] DB error: %s", e)
finally:
if conn: conn.close()
grouped = defaultdict(lambda: defaultdict(list))
for gender, anchor, occ, role, target, reason in rows:
grouped[gender][occ].append({"anchor": anchor, "role": role, "target": target, "reason": reason or ""})
GENDER_META = {
"nu": {"label": "NGA Nu", "color": "#ec4899", "bg": "#fdf2f8", "group": "Neutral+Light+Dark", "colors": "Trang, Hong, Be, Tim, Xanh lam, Den"},
"nam": {"label": "NAM Nam", "color": "#3b82f6", "bg": "#eff6ff", "group": "Neutral (+5 boost)", "colors": "Be, Xam, Den, Trang, Nau, Xanh than"},
"unisex": {"label": "UNI Unisex","color": "#8b5cf6","bg": "#f5f3ff","group": "Neutral+Dark", "colors": "Den, Trang, Xam, Do, Xanh navy, Xanh Jeans"},
"be_gai": {"label": "BG Be Gai","color": "#f43f5e","bg": "#fff1f2","group": "Light Pastel (+5 boost)","colors": "Hong, Tim, Vang nhat, Xanh lam, Trang"},
"be_trai": {"label": "BT Be Trai","color": "#f59e0b","bg": "#fffbeb","group": "Dark (+5 boost)", "colors": "Vang, Cam, Xanh Jeans, Xanh navy, Do"},
}
OCC_LABELS = {"hang_ngay":"Hang ngay","di_lam":"Di lam","di_choi":"Di choi","du_lich":"Du lich","the_thao":"The thao","mac_nha":"Mac nha"}
ROLE_ICONS = {"top":"TOP","bottom":"BTM","outerwear":"OTR","accessory":"ACC"}
sections = ""
for gk, meta in GENDER_META.items():
occ_data = grouped.get(gk, {})
if not occ_data: continue
rows_html = ""
for occ, rlist in occ_data.items():
items = "".join(
f'<span class="ri" title="{r["reason"]}">{r["anchor"]} &rarr; {r["target"]} <em>({ROLE_ICONS.get(r["role"],r["role"])})</em></span>'
for r in rlist)
rows_html += f'<tr><td class="oc"><b>{OCC_LABELS.get(occ,occ)}</b></td><td class="rc">{items}</td></tr>'
sections += f'<div class="gs" style="--c:{meta["color"]};--bg:{meta["bg"]}"><div class="gh"><span class="gtitle">{meta["label"]}</span><span class="gchip">{meta["group"]}</span><span class="gcol">{meta["colors"]}</span></div><table class="rt"><thead><tr><th>Dip mac</th><th>TOP Anchor &rarr; TARGET (role) theo product_line DB</th></tr></thead><tbody>{rows_html}</tbody></table></div>'
html = f"""<!DOCTYPE html><html lang="vi"><head><meta charset="UTF-8"><title>Framework Phoi Do</title><style>
*{{box-sizing:border-box;margin:0;padding:0}}body{{font-family:-apple-system,sans-serif;background:#f8fafc;color:#1e293b;padding:20px}}
.ph{{max-width:1000px;margin:0 auto 20px;padding-bottom:16px;border-bottom:2px solid #e2e8f0}}
.ptitle{{font-size:20px;font-weight:800}}.psub{{font-size:12px;color:#64748b;margin-top:3px}}
.chips{{display:flex;gap:8px;margin-top:8px}}.chip{{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:20px;padding:3px 11px;font-size:12px;font-weight:600;color:#475569}}
.gs{{max-width:1000px;margin:0 auto 16px;border-radius:12px;overflow:hidden;border:1.5px solid var(--c);box-shadow:0 2px 8px rgba(0,0,0,.06)}}
.gh{{background:var(--bg);border-bottom:1.5px solid var(--c);padding:10px 16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}}
.gtitle{{font-size:15px;font-weight:800;color:var(--c)}}.gchip{{background:var(--c);color:#fff;border-radius:20px;padding:2px 8px;font-size:11px;font-weight:700}}.gcol{{font-size:11px;color:#64748b}}
.rt{{width:100%;border-collapse:collapse;background:#fff}}
.rt th{{text-align:left;padding:8px 14px;font-size:10px;text-transform:uppercase;color:#94a3b8;background:#f8fafc;border-bottom:1px solid #e2e8f0}}
.rt tr:not(:last-child) td{{border-bottom:1px solid #f8fafc}}.rt td{{padding:7px 14px;vertical-align:top}}
.oc{{width:110px;font-size:12px;font-weight:700;color:#334155}}.rc{{display:flex;flex-wrap:wrap;gap:5px}}
.ri{{display:inline-flex;align-items:center;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:7px;padding:3px 9px;font-size:12px;font-weight:600;cursor:help;transition:.15s}}
.ri:hover{{background:var(--bg);border-color:var(--c)}}.ri em{{font-size:10px;color:#94a3b8;font-style:normal;margin-left:4px}}
</style></head><body>
<div class="ph"><div class="ptitle">📐 Framework Phối Đồ — Canifa AI Stylist v1.0</div>
<div class="psub">Lưới logic Giới tính × Dịp → nguồn: <code>chatbot_fashion_rules.gender_target</code></div>
<div class="chips"><span class="chip">{len(rows)} rules</span><span class="chip">{len(GENDER_META)} demographic</span></div></div>
{sections}</body></html>"""
return HTMLResponse(content=html)
"""
Fashion Matches — API Route
Manages AI-powered fashion matching (ai_matches) per product.
Endpoints:
GET /api/fashion-matches/{code} → lấy ai_matches
POST /api/fashion-matches/{code}/update → lưu manual edits
POST /api/fashion-matches/{code}/regen → trigger AI engine 1 SP
POST /api/fashion-matches/batch → trigger engine toàn bộ
GET /api/fashion-matches/batch-status → poll batch progress
GET /api/fashion-matches/rules → lấy fashion_rules.json
PUT /api/fashion-matches/rules → cập nhật rules
"""
import json
import logging
import os
import threading
from typing import Optional
from fastapi import APIRouter, BackgroundTasks
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from common.pool_wrapper import get_pooled_connection_compat
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/fashion-matches", tags=["Fashion Matches"])
TABLE = "dashboard_canifa.ultra_descriptions"
RULES_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "worker", "fashion_rules.json")
# ─── Pydantic ────────────────────────────────────────────────
class UpdateMatchesRequest(BaseModel):
ai_matches: dict # full ai_matches payload
class UpdateRulesRequest(BaseModel):
rules: dict
# ─── Helpers ─────────────────────────────────────────────────
def _get_ai_matches(code: str) -> Optional[dict]:
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute(
f"SELECT ai_matches FROM {TABLE} WHERE magento_ref_code = %s OR internal_ref_code = %s LIMIT 1",
(code, code),
)
row = cur.fetchone()
cur.close()
if not row:
return None
data = row[0]
if isinstance(data, str):
data = json.loads(data)
return data or {}
except Exception as e:
logger.error("[FashionMatches] get error %s: %s", code, e)
return None
finally:
if conn:
conn.close()
def _save_ai_matches(code: str, ai_matches: dict) -> bool:
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute(
f"""UPDATE {TABLE}
SET ai_matches = %s::jsonb, updated_at = NOW()
WHERE magento_ref_code = %s OR internal_ref_code = %s""",
(json.dumps(ai_matches, ensure_ascii=False), code, code),
)
updated = cur.rowcount > 0
conn.commit()
cur.close()
return updated
except Exception as e:
if conn:
conn.rollback()
logger.error("[FashionMatches] save error %s: %s", code, e)
return False
finally:
if conn:
conn.close()
def _run_engine_background(code: Optional[str] = None):
"""Background task: run engine for 1 code or full batch."""
try:
from worker.stylist_engine import StylistEngine
engine = StylistEngine()
if code:
engine.run_for_code(code)
else:
engine.run_batch()
except Exception as e:
logger.error("[FashionMatches] engine error: %s", e)
# ─── Endpoints ───────────────────────────────────────────────
@router.get("/{code}")
async def get_fashion_matches(code: str):
"""GET ai_matches for a single product."""
data = _get_ai_matches(code)
if data is None:
return JSONResponse({"ok": False, "error": "Product not found"}, status_code=404)
return {"ok": True, "code": code, "ai_matches": data}
@router.post("/{code}/update")
async def update_fashion_matches(code: str, req: UpdateMatchesRequest):
"""Save manually edited ai_matches."""
ok = _save_ai_matches(code, req.ai_matches)
if ok:
logger.info("[FashionMatches] Manual update saved: %s", code)
return {"ok": True, "message": "Đã lưu gợi ý phối đồ"}
return JSONResponse({"ok": False, "error": "Không tìm thấy sản phẩm"}, status_code=404)
@router.post("/{code}/regen")
async def regen_fashion_matches(code: str, background_tasks: BackgroundTasks):
"""Trigger AI engine for a single product (async background)."""
background_tasks.add_task(_run_engine_background, code)
logger.info("[FashionMatches] Regen triggered: %s", code)
return {"ok": True, "message": f"Đang tính toán phối đồ cho {code}..."}
@router.post("/batch")
async def batch_fashion_matches(background_tasks: BackgroundTasks):
"""Trigger full batch engine run."""
from worker.stylist_engine import BATCH_STATE
if BATCH_STATE.get("is_running"):
return JSONResponse({"ok": False, "error": "Batch đang chạy, vui lòng chờ"}, status_code=409)
background_tasks.add_task(_run_engine_background, None)
return {"ok": True, "message": "Đã khởi động batch Stylist Engine..."}
@router.get("/batch/status")
async def batch_status():
"""Poll batch progress."""
from worker.stylist_engine import BATCH_STATE
state = dict(BATCH_STATE)
pct = 0
if state.get("total", 0) > 0:
pct = round(state["done"] / state["total"] * 100, 1)
state["progress_pct"] = pct
return {"ok": True, "state": state}
@router.get("/rules/config")
async def get_rules():
"""Fetch current fashion_rules.json."""
try:
with open(RULES_PATH, "r", encoding="utf-8") as f:
rules = json.load(f)
return {"ok": True, "rules": rules}
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
@router.put("/rules/config")
async def update_rules(req: UpdateRulesRequest):
"""Save updated fashion_rules.json."""
try:
with open(RULES_PATH, "w", encoding="utf-8") as f:
json.dump(req.rules, f, ensure_ascii=False, indent=2)
return {"ok": True, "message": "Rules đã được cập nhật"}
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
class ScoreTestRequest(BaseModel):
source_code: str
target_code: str
@router.post("/score-test")
async def score_test(req: ScoreTestRequest):
"""Test pairwise match score between two products — returns full breakdown."""
try:
from worker.stylist_engine import StylistEngine
engine = StylistEngine()
catalog = engine._load_catalog()
source = next((p for p in catalog if p.get("code") == req.source_code), None)
target = next((p for p in catalog if p.get("code") == req.target_code), None)
if not source:
return JSONResponse({"ok": False, "error": f"Không tìm thấy SP nguồn: {req.source_code}"}, status_code=404)
if not target:
return JSONResponse({"ok": False, "error": f"Không tìm thấy SP đích: {req.target_code}"}, status_code=404)
# Compute score with breakdown
breakdown = engine._score_breakdown(source, target)
total = sum(v["score"] for v in breakdown.values())
occ0 = (engine.rules.get("occasions") or ["hang_ngay"])[0]
reason = engine._build_reason(source, target, occ0, total)
return {
"ok": True,
"result": {
"source_code": req.source_code,
"target_code": req.target_code,
"target_name": target.get("name", ""),
"roles_str": target.get("product_line", ""),
"total_score": total,
"reason": reason,
"breakdown": breakdown,
},
}
except Exception as e:
logger.error("[ScoreTest] error: %s", e)
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
This diff is collapsed.
B [DB] 🔌 Đang kết nối StarRocks (New Session): 172.16.2.100:9030... B [DB] 🔌 Đang kết nối StarRocks (New Session): 172.16.2.100:9030...
...@@ -33,6 +33,11 @@ class CanifaDbPool: ...@@ -33,6 +33,11 @@ class CanifaDbPool:
with db_pool.get_conn() as conn: with db_pool.get_conn() as conn:
conn.execute(...) conn.execute(...)
""" """
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from .sqlite_mock import get_mock_pg_conn
return get_mock_pg_conn()
if not self._pool: if not self._pool:
self.initialize() self.initialize()
return self._pool.connection() return self._pool.connection()
......
...@@ -113,6 +113,13 @@ class StarRocksConnection: ...@@ -113,6 +113,13 @@ class StarRocksConnection:
return self.conn return self.conn
def execute_query(self, query: str, params: tuple | None = None) -> list[dict[str, Any]]: def execute_query(self, query: str, params: tuple | None = None) -> list[dict[str, Any]]:
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from .sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(query, params)
return mc.fetchall()
# print(" [DB] 🚀 Bắt đầu truy vấn dữ liệu...") # print(" [DB] 🚀 Bắt đầu truy vấn dữ liệu...")
# (Reduced noise in logs) # (Reduced noise in logs)
logger.info("🚀 Executing StarRocks Query (Persistent Conn).") logger.info("🚀 Executing StarRocks Query (Persistent Conn).")
...@@ -182,6 +189,13 @@ class StarRocksConnection: ...@@ -182,6 +189,13 @@ class StarRocksConnection:
""" """
Execute query asynchronously with AUTO-RECONNECT (Fix lỗi 10053/2006). Execute query asynchronously with AUTO-RECONNECT (Fix lỗi 10053/2006).
""" """
from config import USE_LOCAL_SQLITE
if USE_LOCAL_SQLITE:
from .sqlite_mock import MockCursor
mc = MockCursor()
mc.execute(query, params)
return mc.fetchall()
max_retries = 3 max_retries = 3
for attempt in range(max_retries): for attempt in range(max_retries):
......
...@@ -160,3 +160,6 @@ RATE_LIMIT_USER: int = int(os.getenv("RATE_LIMIT_USER", "100")) ...@@ -160,3 +160,6 @@ RATE_LIMIT_USER: int = int(os.getenv("RATE_LIMIT_USER", "100"))
STOCK_API_URL: str = os.getenv("STOCK_API_URL", "https://canifa.com/v1/middleware/stock_get_stock_list") STOCK_API_URL: str = os.getenv("STOCK_API_URL", "https://canifa.com/v1/middleware/stock_get_stock_list")
# Internal Stock API (có logic expand SKU từ base code) # Internal Stock API (có logic expand SKU từ base code)
INTERNAL_STOCK_API: str = os.getenv("INTERNAL_STOCK_API", "http://localhost:5000/api/stock/check") INTERNAL_STOCK_API: str = os.getenv("INTERNAL_STOCK_API", "http://localhost:5000/api/stock/check")
# ====================== LOCAL SQLITE OVERRIDE ======================
USE_LOCAL_SQLITE: bool = os.getenv("USE_LOCAL_SQLITE", "true").lower() == "true"
"""
Airflow 3 DAG: Migration and Sync Pipeline for AI Law Bot
Extracts law text from local MongoDB and upserts into the new PostgreSQL database.
Embeddings will be automatically handled by `pgai` vectorizer within Postgres.
"""
from datetime import datetime, timedelta
from airflow.decorators import dag, task
from airflow.providers.mongo.hooks.mongo import MongoHook
from airflow.providers.postgres.hooks.postgres import PostgresHook
import pandas as pd
# Define Default Arguments
default_args = {
'owner': 'ai_team',
'retries': 3,
'retry_delay': timedelta(minutes=5),
}
@dag(
dag_id='ai_law_sync_to_postgres',
default_args=default_args,
description='Syncs law documents from MongoDB to PostgreSQL for pgai vectorization',
schedule_interval='@daily',
start_date=datetime(2026, 4, 1),
catchup=False,
tags=['law_bot', 'ai', 'migration'],
)
def ai_law_postgres_sync():
@task()
def initialize_postgres_schema():
"""
Ensures the target PostgreSQL table exists.
Note: The pgai vectorizer should be configured independently on the database level.
"""
pg_hook = PostgresHook(postgres_conn_id='canifa_postgres_138')
schema_query = """
CREATE TABLE IF NOT EXISTS law_documents (
id VARCHAR(255) PRIMARY KEY,
title TEXT,
content TEXT NOT NULL,
category VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
pg_hook.run(schema_query)
print("Schema initialized.")
@task(multiple_outputs=True)
def extract_from_mongodb() -> dict:
"""
Extracts law documents from MongoDB (`ai_law` database).
"""
mongo_hook = MongoHook(mongo_conn_id='local_mongo_law')
client = mongo_hook.get_conn()
db = client.ai_law
collection = db.law_records
# In a real scenario, you'd fetch based on a last_sync_date (incremental loading).
# For this template, we fetch a batch of records.
records = list(collection.find({}, {'_id': 1, 'title': 1, 'content': 1, 'category': 1}).limit(10000))
# Convert _id ObjectId to string format
for record in records:
record['id'] = str(record.pop('_id'))
print(f"Extracted {len(records)} documents from MongoDB.")
return {'records': records}
@task()
def load_to_postgres(extracted_data: dict):
"""
Upserts the extracted data into PostgreSQL.
"""
records = extracted_data.get('records', [])
if not records:
print("No records to insert. Skipping.")
return
# Convert to Pandas DataFrame for efficient bulk insertion
df = pd.DataFrame(records)
pg_hook = PostgresHook(postgres_conn_id='canifa_postgres_138')
engine = pg_hook.get_sqlalchemy_engine()
# Temporary table logic or direct 'to_sql' based on upsert preference.
# This uses direct append for simplicity; for real upsert, use SQLAlchemy raw SQL
df.to_sql('law_documents', con=engine, if_exists='append', index=False, method='multi', chunksize=1000)
print(f"Successfully loaded {len(records)} records into PostgreSQL.")
# DAG Execution Flow
schema_setup = initialize_postgres_schema()
extracted_data = extract_from_mongodb()
schema_setup >> extracted_data
load_to_postgres(extracted_data)
# Instantiate the DAG
dag = ai_law_postgres_sync()
[
{
"day": 0,
"hour": 9,
"minute": 24,
"label": "Thứ 2 sáng"
},
{
"day": 0,
"hour": 11,
"minute": 10,
"label": "Thứ 2 trưa"
},
{
"day": 1,
"hour": 9,
"minute": 55,
"label": "Thứ 3 sáng"
},
{
"day": 2,
"hour": 9,
"minute": 30,
"label": "Thứ 4 sáng"
},
{
"day": 2,
"hour": 11,
"minute": 32,
"label": "Thứ 4 trưa"
},
{
"day": 3,
"hour": 9,
"minute": 38,
"label": "Thứ 5 sáng"
},
{
"day": 4,
"hour": 10,
"minute": 17,
"label": "Thứ 6 sáng"
},
{
"day": 4,
"hour": 14,
"minute": 0,
"label": "Thứ 6 chiều"
},
{
"day": 5,
"hour": 10,
"minute": 0,
"label": "Thứ 7 sáng"
}
]
\ No newline at end of file
This diff is collapsed.
"""Patch TAG_TO_BITMAP_COL với giá trị thực tế từ DB."""
import re
path = r'D:\cnf\chatbot-canifa-feedback\backend\agent\tag_search_agent_v2\tag_search_tool.py'
with open(path, encoding='utf-8') as f:
content = f.read()
# Find and replace the TAG_TO_BITMAP_COL block using regex
pattern = r'(TAG_TO_BITMAP_COL: dict\[str, tuple\[str, str\]\] = \{)[^}]+(})'
replacement = r'''TAG_TO_BITMAP_COL: dict[str, tuple[str, str]] = {
# Trục 3 — style (cột `style` có BITMAP index)
# DB values: Street, Basic Update, Feminine, Basic, Athleisure, Cute, Essential, Smart Casual, Utility, Trend, Dynamic, Preppy
"style:thanh_lich": ("style", "Feminine"),
"style:nang_dong": ("style", "Dynamic"),
"style:basic": ("style", "Basic"),
"style:ca_tinh": ("style", "Street"),
"style:de_thuong": ("style", "Cute"),
"style:tre_trung": ("style", "Trend"),
"style:toi_gian": ("style", "Essential"),
"style:smart_casual":("style", "Smart Casual"),
# Trục 4 — fitting (cột `fitting` có BITMAP index)
# DB values: Boxy, Skinny, Oversize, Relax, Slimfit, Regular, Slim
"fit:oversize": ("fitting", "Oversize"),
"fit:slim": ("fitting", "Slim"),
"fit:regular": ("fitting", "Regular"),
"fit:wide_leg": ("fitting", "Relax"),
"fit:cropped": ("fitting", "Boxy"),
"fit:relaxed": ("fitting", "Relax"),
# Trục 2a — season_sale (cột `season_sale` có BITMAP index)
# DB values thực tế: Year, Winter, Summer
"wthr:mua_he": ("season_sale", "Summer"),
"wthr:mua_dong": ("season_sale", "Winter"),
}'''
new_content, n = re.subn(pattern, replacement, content, flags=re.DOTALL)
if n > 0:
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f'✅ Patched {n} occurrence(s)')
else:
print('❌ Pattern not found — printing lines 360-385:')
for i, line in enumerate(content.splitlines()[359:385], 360):
print(f'{i}: {line}')
"""Migration: add ai_matches JSONB column to ultra_descriptions."""
from common.pool_wrapper import get_pooled_connection_compat
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute("""
ALTER TABLE dashboard_canifa.ultra_descriptions
ADD COLUMN IF NOT EXISTS ai_matches JSONB DEFAULT '{}'::jsonb;
""")
conn.commit()
cur.close()
conn.close()
print("OK: ai_matches column added to ultra_descriptions")
"""
DashboardAI Prompt — Generates diverse dashboard layouts from natural language.
Supports 2 databases: StarRocks (product catalog) + PostgreSQL (chat history).
Imported by sql_chat_route.py.
"""
STARROCKS_TABLE = "shared_source.magento_product_dimension_with_text_embedding"
POSTGRES_TABLE = "public.langgraph_chat_histories"
DB_SCHEMA = f"""
## Database 1: StarRocks (db: "starrocks")
## Table: {STARROCKS_TABLE}
### Columns (SELECT only, KHÔNG query cột 'vector'):
- internal_ref_code (VARCHAR) — Mã sản phẩm nội bộ, VD: "8TP25A005"
- magento_ref_code (VARCHAR) — Mã ref = internal_ref_code + color, VD: "8TP25A005-SW011"
- product_color_code (VARCHAR) — Mã màu sản phẩm
- product_name (VARCHAR) — Tên sản phẩm, VD: "ÁO THUN NAM"
- color_code (VARCHAR) — Mã màu
- master_color (VARCHAR) — Nhóm màu chính: "Trắng", "Đen", "Đỏ", etc.
- product_color_name (VARCHAR) — Tên màu đầy đủ
- season_sale (VARCHAR) — Mùa sale
- season (VARCHAR) — Mùa: "Xuân Hè", "Thu Đông", etc.
- style (VARCHAR) — Phong cách sản phẩm
- fitting (VARCHAR) — Kiểu form: "Regular Fit", "Slim Fit", etc.
- size_scale (VARCHAR) — Thang size: "XS-XL", "1-6", etc.
- graphic (VARCHAR) — Họa tiết
- pattern (VARCHAR) — Hoa văn
- weaving (VARCHAR) — Kiểu dệt
- shape_detail (VARCHAR) — Chi tiết kiểu dáng
- form_neckline (VARCHAR) — Kiểu cổ: "Cổ tròn", "Cổ V", etc.
- form_sleeve (VARCHAR) — Kiểu tay: "Ngắn tay", "Dài tay", etc.
- form_length (VARCHAR) — Kiểu dài
- form_waistline (VARCHAR) — Kiểu eo
- form_shoulderline (VARCHAR) — Kiểu vai
- form_pants (VARCHAR) — Kiểu quần
- material (VARCHAR) — Chất liệu
- specific_material (VARCHAR) — Chất liệu chi tiết
- sale_price (DECIMAL) — Giá bán (VND)
- original_price (DECIMAL) — Giá gốc (VND)
- discount_amount (DECIMAL) — Giá giảm (VND)
- quantity_sold (INT) — Số lượng đã bán
- is_new_product (TINYINT) — Sản phẩm mới (1=new)
- gender_by_product (VARCHAR) — Giới tính: "men", "women", "boy", "girl", "unisex"
- product_line_vn (VARCHAR) — Dòng sản phẩm: "Áo phông", "Quần short", etc.
- product_web_url (VARCHAR) — URL web
---
## Database 2: PostgreSQL (db: "postgres")
## Table: {POSTGRES_TABLE}
This table stores ALL chatbot conversations (user messages + AI responses).
### Columns:
- id (SERIAL) — Auto-increment ID
- identity_key (VARCHAR 255) — User/session identifier
- message (TEXT) — Content: plain text for user messages, JSON string for AI responses
- AI response JSON format: {{"ai_response": "...", "product_ids": [...]}}
- User message: plain text, e.g. "tôi muốn mua áo polo"
- is_human (BOOLEAN) — true = user message, false = AI response
- timestamp (TIMESTAMPTZ) — Message timestamp
### Important Notes for querying chat history:
- To search user questions: WHERE is_human = true AND message ILIKE '%keyword%'
- To count unique users: COUNT(DISTINCT identity_key)
- To count conversations about a topic: COUNT(*) WHERE is_human = true AND message ILIKE '%topic%'
- message for AI responses is JSON, use message::json->>'ai_response' to extract AI text
- Common topics: "áo polo", "áo phông", "quần", "váy", "đầm", "áo khoác", "áo lót"
- Use timestamp for date filtering: WHERE timestamp >= '2026-01-01'
"""
DASHBOARD_PROMPT = f"""You are DashboardAI, an intelligent analytics dashboard generator for Canifa (Vietnamese fashion brand).
When a user describes a report or dashboard, respond ONLY with a raw JSON object (no markdown, no explanation).
{DB_SCHEMA}
## ⚡ CRITICAL: Each widget MUST include "db" field ⚡
- "db": "starrocks" — for product catalog queries (table: {STARROCKS_TABLE})
- "db": "postgres" — for chat history queries (table: {POSTGRES_TABLE})
## RULES:
- ONLY SELECT queries. NEVER INSERT/UPDATE/DELETE/DROP/ALTER/CREATE.
- Always LIMIT (max 200 rows per widget).
- NEVER query 'embedding' or 'vector' columns.
- Each widget MUST have a "db" field indicating which database to query.
- For product data: use StarRocks table {STARROCKS_TABLE}
- For chat/conversation data: use PostgreSQL table {POSTGRES_TABLE}
- You can combine both databases in one dashboard (e.g., product KPIs + chat analytics)
## Widget JSON Schema:
Each widget has: id, type, title, size, color, sql, x_key, y_key, db
## Available Types:
- **kpi**: Single metric (COUNT/SUM/AVG/MAX/MIN). SQL returns 1 row 1 col.
- **bar**: Vertical bar chart. SQL returns x_key + y_key columns.
- **horizontal-bar**: Horizontal bar chart. Same data as bar.
- **stacked-bar**: Stacked bar chart. SQL returns x_key + multiple numeric cols. Use y_keys (array).
- **line**: Line chart. SQL returns x_key + y_key.
- **area**: Filled area chart. Same as line.
- **donut**: Donut/pie chart. SQL returns label + value.
- **scatter**: Scatter plot. SQL returns x_key + y_key (both numeric).
- **table**: Data table. SQL returns any columns.
- **progress**: Progress bars showing completion. SQL returns label + current + max columns. y_keys: ["current_col","max_col"].
- **number-row**: Row of 2-3 inline metrics. SQL returns 1 row with 2-3 numeric cols.
## Available Sizes (12-column grid):
- "xs": 2 columns — tiny metric
- "sm": 3 columns — compact KPI
- "md": 4 columns — standard card
- "half": 6 columns — half width
- "lg": 8 columns — wide chart
- "full": 12 columns — full width
## Available Colors:
indigo, emerald, amber, red, purple, cyan, pink, orange, teal, blue
## ⚡ CRITICAL: Layout MUST BE DIVERSE! ⚡
You MUST vary the layout. NEVER produce the same layout twice. Here are 6 patterns — randomly pick one or create your own mix:
### Pattern A: "KPI Row + Mixed Charts"
Row 1: 4× kpi (sm) | Row 2: 1× bar (half) + 1× donut (half) | Row 3: 1× line (full) | Row 4: 1× table (full)
### Pattern B: "Hero KPI + Detail Grid"
Row 1: 1× kpi (md) + 1× kpi (md) + 1× kpi (md) | Row 2: 1× area (lg) + 1× number-row (md) | Row 3: 1× horizontal-bar (half) + 1× donut (half) | Row 4: 1× table (full)
### Pattern C: "Wide Charts Focus"
Row 1: 3× kpi (md) | Row 2: 1× bar (full) | Row 3: 1× donut (md) + 1× line (lg) | Row 4: 1× table (full)
### Pattern D: "Compact Dashboard"
Row 1: 2× kpi (half) | Row 2: 1× horizontal-bar (half) + 1× progress (half) | Row 3: 1× area (half) + 1× donut (half) | Row 4: 1× table (full)
### Pattern E: "Analytics Deep Dive"
Row 1: 4× kpi (sm) | Row 2: 1× scatter (half) + 1× bar (half) | Row 3: 1× stacked-bar (full) | Row 4: 1× table (full)
### Pattern F: "Executive Summary"
Row 1: 1× kpi (half) + 1× number-row (half) | Row 2: 1× area (full) | Row 3: 1× donut (md) + 1× horizontal-bar (lg) | Row 4: 1× table (full)
## Tips:
- Mix sizes! Don't make all KPIs the same size.
- Use horizontal-bar for categorical comparisons (top 10 products, etc.)
- Use scatter when comparing two numeric dimensions (price vs quantity)
- Use progress for goal-tracking or top-N with % share
- Use stacked-bar to show composition across categories
- number-row is great for showing 2-3 metrics in one compact widget
- Always produce 5-10 widgets total
- RANDOMIZE which pattern you use. Be creative!
- Respond ONLY with the raw JSON. No other text.
"""
This diff is collapsed.
"""
Report Agent Prompts — ReAct loop with reflect for AI Report generation.
Imported by api/report_agent_route.py
"""
from prompts.dashboard_prompt import STARROCKS_TABLE, POSTGRES_TABLE, DB_SCHEMA
# ═══════════════════════════════════════════════════════════════════
# LANGFUSE TABLE (in StarRocks)
# ═══════════════════════════════════════════════════════════════════
LANGFUSE_TABLE = "analytic.chatbot_rsa_trace_event_detail"
LANGFUSE_SCHEMA = f"""
## Table: {LANGFUSE_TABLE} (StarRocks — Langfuse Analytics)
Columns:
- trace_id (VARCHAR), project_name (VARCHAR), org_name (VARCHAR)
- session_id (VARCHAR), customer_id (VARCHAR), device_id (VARCHAR)
- trace_latency (DECIMAL — seconds), user_latency (DECIMAL — seconds)
- input_cost (DECIMAL — USD), output_cost (DECIMAL — USD), total_cost (DECIMAL — USD)
- model_name (VARCHAR — e.g. 'gpt-5.2-codex', 'gemini-3.1-flash-lite')
- total_obs (BIGINT), total_obs_error (BIGINT), nb_generation_error (BIGINT)
- is_guest (TINYINT — 1/0), is_user (TINYINT — 1/0)
- traced_at (DATETIME), created_at (DATETIME), updated_at (DATETIME)
"""
# ═══════════════════════════════════════════════════════════════════
# AGENT PROMPT — ReAct loop with reflection
# ═══════════════════════════════════════════════════════════════════
AGENT_PROMPT = f"""You are an AI Data Analyst Agent for Canifa (Vietnamese fashion brand).
You work in a ReAct loop: THINK → ACT → OBSERVE → REFLECT → (repeat or finish).
## AVAILABLE TOOLS:
1. **sql_langfuse** — Query `{LANGFUSE_TABLE}` on StarRocks
Use for: chatbot usage, costs, latency, models, errors, users
{LANGFUSE_SCHEMA}
2. **sql_starrocks** — Query `{STARROCKS_TABLE}` on StarRocks
Use for: product analytics, sales, categories. NEVER SELECT 'vector'/'embedding' columns.
Columns: product_name, sale_price, original_price, quantity_sold, gender_by_product, product_line_vn, master_color, material, season, is_new_product, etc.
3. **sql_postgres** — Query `{POSTGRES_TABLE}` on PostgreSQL
Use for: chat history, user messages, conversation analysis.
Columns: identity_key, message, is_human, timestamp
4. **calculator** — Pure math only (digits + -, *, /, parentheses). No text in expression.
## YOUR TASK:
Given the user's question, iteratively gather data until you have ENOUGH for a comprehensive report.
## RESPONSE FORMAT (JSON):
At each step, respond with ONE of these:
### Option 1: Execute tools (gather more data)
```json
{{
"action": "execute",
"thinking": "What I need to find out and why",
"tools": [
{{"name": "tool_name", "params": {{"sql": "SELECT ..."}}, "purpose": "Why this query"}}
]
}}
```
### Option 2: Reflect on collected data
```json
{{
"action": "reflect",
"thinking": "Assessment of data collected so far",
"data_sufficient": true/false,
"missing": ["What data is still missing (if any)"],
"quality_issues": ["Data quality concerns (if any)"],
"next_tools": [
{{"name": "tool_name", "params": {{"sql": "SELECT ..."}}, "purpose": "Why this additional query"}}
]
}}
```
- If data_sufficient=true AND missing=[] → agent will proceed to write report
- If data_sufficient=false → next_tools will be executed, then another reflect cycle
### Option 3: Write the final report
```json
{{
"action": "write_report"
}}
```
(Only output this when told all data is ready)
## RULES:
- Start with 2-4 diverse queries covering different aspects of the user's question
- After seeing results, REFLECT: is the data enough? Good quality? Need deeper analysis?
- If data has gaps, errors, or insufficient depth → add more targeted queries
- Maximum 3 reflect cycles (to prevent infinite loops)
- For calculator: expression must contain ONLY digits and math operators
- All SQL must be SELECT-only
- Be thorough but efficient — don't over-query
RESPOND WITH RAW JSON ONLY. No markdown.
"""
# ═══════════════════════════════════════════════════════════════════
# WRITER — Generates the full report JSON from real data
# ═══════════════════════════════════════════════════════════════════
WRITER_PROMPT = """You are an expert business analyst and report writer for Canifa (Vietnamese fashion brand).
You have received REAL DATA from various data tools. Write a comprehensive analytical report.
## CRITICAL: USE REAL DATA ONLY
- All numbers, charts, and tables MUST come from the provided tool results
- NEVER fabricate or estimate numbers that aren't in the data
- If data is missing for a section, note it honestly
## REPORT JSON SCHEMA:
{
"title": "string — full report title",
"subtitle": "string — descriptive subtitle",
"organization": "string — e.g. 'Phòng Phân tích AI — Canifa'",
"period": "string — reporting period",
"prepared_by": "string — 'AI Report Agent · Canifa AI Platform'",
"executive_summary": "string — 3–5 sentences summarizing key findings from REAL data",
"highlights": [ { "label": "string", "value": "string (from data)", "trend": "up|down|neutral" } ],
"sections": [ Section ],
"conclusion": "string — 2–4 sentences based on findings",
"recommendations": [ "string — actionable, based on data" ]
}
Section = {
"id": "string",
"number": "number (1, 2, 3...)",
"title": "string",
"paragraphs": [ "string — analytical text using REAL numbers from data" ],
"chart": Chart | null,
"table": Table | null,
"callout": { "type": "info|warning|success", "text": "string" } | null
}
Chart = {
"type": "bar|line|area|donut|hbar",
"title": "string",
"caption": "string — 1 sentence explaining what the chart shows",
"labels": ["string"],
"datasets": [ { "label": "string", "values": [number] } ],
"segments": [ { "name": "string", "value": number } ]
}
Table = {
"title": "string",
"caption": "string",
"columns": ["string"],
"rows": [["string"]]
}
## CONTENT RULES:
- Write each paragraph as analytical text (3–5 sentences), referencing REAL data
- 4–6 sections, each focused on one aspect
- Each section should have 2–3 paragraphs
- Put a chart OR table in most sections (not both unless very relevant)
- highlights: 4–6 key KPIs from the data
- recommendations: 4–6 actionable points based on findings
- All text in Vietnamese
- Chart data MUST match the tool results — do NOT invent numbers
- For donut charts use "segments", for other types use "labels" + "datasets"
RESPOND WITH RAW JSON ONLY. No markdown fences.
"""
"""
AI SQL Trace Prompt.
Dedicated prompt for a standalone analyst agent whose primary output is
the reasoning trace and SQL execution plan, not a business report.
"""
from prompts.dashboard_prompt import POSTGRES_TABLE, STARROCKS_TABLE
from prompts.report_html_prompt import LANGFUSE_SCHEMA, LANGFUSE_TABLE
SQL_TRACE_AGENT_PROMPT = f"""You are a Senior AI SQL Analyst for Canifa.
Your job is NOT to write a report. Your job is to analyze the user's question,
plan the investigation, generate SQL queries, inspect results, reflect on gaps,
and stop once the analysis path is clear.
You operate in a strict loop:
THINK -> PLAN -> ACT -> OBSERVE -> REFLECT -> repeat if needed
## PRIMARY GOAL
Return a structured, auditable analysis trace that explains:
- what the user is asking
- what sub-task is being investigated
- what SQL should be executed
- why each SQL exists
- whether the result is sufficient
- what follow-up SQL is still needed
## DATA SOURCES
1. sql_langfuse — Query `{LANGFUSE_TABLE}` on StarRocks
{LANGFUSE_SCHEMA}
2. sql_starrocks — Query `{STARROCKS_TABLE}` on StarRocks
Use for product and sales analytics. NEVER query vector/embedding columns.
3. sql_postgres — Query `{POSTGRES_TABLE}` on PostgreSQL
Use for chat history and report metadata.
4. calculator — Pure arithmetic only.
## SQL RULES
- SELECT only
- Every query MUST end with LIMIT 20
- Prefer aggregate SQL over raw rows
- Group by day/week/month for time trends
- Compute ratios and percentages in SQL when possible
- NEVER query embedding/vector columns
- If you need percentile on DECIMAL columns in StarRocks, CAST the metric to DOUBLE first
Example: approx_percentile(CAST(trace_latency AS DOUBLE), 0.95)
## ANALYSIS DEPTH
- First cycle: overview and framing queries
- Later cycles: drill-down, comparison, anomaly checks, root-cause validation
- Stop when the investigation is analytically sufficient
- Maximum 4 cycles
## RESPONSE FORMAT
Always respond with raw JSON only.
### When you want to run SQL:
{{
"action": "execute",
"thinking": "Short Vietnamese reasoning for why these queries are needed",
"current_task": "1. Tổng quan chi phí",
"sub_tasks": [
"1.1 Chi phí theo ngày",
"1.2 Cơ cấu chi phí theo model"
],
"tools": [
{{
"name": "sql_langfuse",
"params": {{"sql": "SELECT ... LIMIT 20"}},
"purpose": "1.1 Lấy xu hướng chi phí theo ngày để xác định spike"
}}
]
}}
### When you are reflecting on collected data:
{{
"action": "reflect",
"thinking": "Short Vietnamese assessment of what was learned",
"current_task": "1. Tổng quan chi phí",
"completed_sub_tasks": [
"1.1 Đã có xu hướng chi phí theo ngày",
"1.2 Đã có top model theo tổng cost"
],
"data_sufficient": false,
"drill_down_opportunities": [
"Ngày 2026-03-18 có total_cost cao bất thường cần drill-down theo model"
],
"missing": [
"P95 latency theo model",
"Tỷ trọng input/output cost theo model"
],
"next_task": "2. Drill-down ngày đỉnh chi phí",
"next_sub_tasks": [
"2.1 Top model trong ngày spike",
"2.2 So sánh input/output cost"
],
"next_tools": [
{{
"name": "sql_langfuse",
"params": {{"sql": "SELECT ... LIMIT 20"}},
"purpose": "2.1 Phân rã ngày spike theo model"
}}
]
}}
## QUALITY BAR
Your output should feel like an expert analyst's working session:
- broad enough to frame the problem
- focused enough to avoid wasteful queries
- explicit about why each query exists
- explicit about whether the data is enough
Respond in Vietnamese.
Respond with raw JSON only.
"""
...@@ -134,3 +134,5 @@ slowapi==0.1.9 ...@@ -134,3 +134,5 @@ slowapi==0.1.9
PyJWT==2.12.1 PyJWT==2.12.1
passlib==1.7.4 passlib==1.7.4
bcrypt==5.0.0 bcrypt==5.0.0
asyncpg==0.31.0
python-multipart==0.0.26
"""Seed flow_items with proper UTF-8 via HTTP API."""
import urllib.request, json
BASE = "http://localhost:5000/api/dashboard/flow"
# Delete all existing
resp = urllib.request.urlopen(BASE)
data = json.loads(resp.read())
for item in data.get("items", []):
req = urllib.request.Request(f"{BASE}/{item['id']}", method="DELETE")
urllib.request.urlopen(req)
print("Cleared all existing flow items")
items = [
{"id":"f01","title":"Kh\u00e1ch g\u1eedi tin nh\u1eafn","description":"Kh\u00e1ch h\u00e0ng nh\u1eadp c\u00e2u h\u1ecfi qua chatbot widget tr\u00ean web/app","category":"input","parent_id":None,"sort_order":1,"icon":"\ud83d\udcac"},
{"id":"f02","title":"API Gateway","description":"FastAPI nh\u1eadn request, x\u00e1c th\u1ef1c, rate limit","category":"process","parent_id":None,"sort_order":2,"icon":"\ud83d\udd0c"},
{"id":"f03","title":"Ki\u1ec3m tra cache","description":"Tra b\u1ed9 nh\u1edb t\u1ea1m \u2014 c\u00e2u h\u1ecfi n\u00e0y \u0111\u00e3 \u0111\u01b0\u1ee3c h\u1ecfi ch\u01b0a?","category":"process","parent_id":None,"sort_order":3,"icon":"\u26a1"},
{"id":"f04","title":"L\u1ea5y l\u1ecbch s\u1eed + h\u1ed3 s\u01a1 kh\u00e1ch","description":"10 tin nh\u1eafn g\u1ea7n nh\u1ea5t + h\u1ed3 s\u01a1 d\u00e0i h\u1ea1n (size, s\u1edf th\u00edch)","category":"process","parent_id":None,"sort_order":4,"icon":"\ud83d\udcbe"},
{"id":"f05","title":"AI Agent suy ngh\u0129","description":"Gemini ph\u00e2n t\u00edch \u00fd \u0111\u1ecbnh, quy\u1ebft \u0111\u1ecbnh c\u1ea7n tra c\u1ee9u g\u00ec","category":"process","parent_id":None,"sort_order":5,"icon":"\ud83e\udde0"},
{"id":"f06","title":"Product Search (Vector)","description":"T\u00ecm s\u1ea3n ph\u1ea9m b\u1eb1ng semantic search + SQL filters","category":"tool","parent_id":"f05","sort_order":6,"icon":"\ud83d\udd0d"},
{"id":"f07","title":"Stock Check","description":"Ki\u1ec3m tra t\u1ed3n kho realtime qua API Magento","category":"tool","parent_id":"f05","sort_order":7,"icon":"\ud83d\udce6"},
{"id":"f08","title":"Knowledge Base","description":"Tra ch\u00ednh s\u00e1ch \u0111\u1ed5i tr\u1ea3, khuy\u1ebfn m\u00e3i, th\u00f4ng tin c\u1eeda h\u00e0ng","category":"tool","parent_id":"f05","sort_order":8,"icon":"\ud83d\udcda"},
{"id":"f09","title":"Streaming Response","description":"Tr\u1ea3 l\u1eddi t\u1eebng ch\u1eef nh\u01b0 ChatGPT, k\u00e8m product cards","category":"output","parent_id":None,"sort_order":9,"icon":"\ud83d\udce1"},
{"id":"f10","title":"Background Tasks","description":"L\u01b0u l\u1ecbch s\u1eed, c\u1eadp nh\u1eadt h\u1ed3 s\u01a1 kh\u00e1ch, cache, log Langfuse","category":"output","parent_id":None,"sort_order":10,"icon":"\u2699\ufe0f"},
{"id":"f11","title":"Langfuse Trace","description":"Log observability: latency, tokens, cost, scores","category":"monitor","parent_id":None,"sort_order":11,"icon":"\ud83d\udcca"},
]
for item in items:
body = json.dumps(item).encode("utf-8")
req = urllib.request.Request(BASE, data=body, headers={"Content-Type":"application/json"}, method="POST")
urllib.request.urlopen(req)
print(f"Seeded {len(items)} flow items OK")
"""Seed roadmap notes via the notes API."""
import urllib.request, json, time
BASE = "http://localhost:5000/api/dashboard/notes"
notes = [
{"title": "v1.0 \u2014 Production", "content": """Model: GPT-4.1-mini
Search: Vector search (pgvector, IVFFlat)
Stock: API Magento realtime
Prompt: Module v1 (persona, guardrails, sales flow)
Observability: Langfuse tracing + scoring
Chat: L\u01b0u l\u1ecbch s\u1eed + Like/Dislike feedback""", "category": "note", "pinned": True},
{"title": "v1.1 \u2014 \u0110\u1ed5i model (th\u1eed nghi\u1ec7m)", "content": """Chuy\u1ec3n GPT-4.1-mini \u2192 Gemini 3.1 Flash-Lite
Gi\u1ea3m ~55% chi ph\u00ed per request
R\u00fat g\u1ecdn prompt 56%, format ph\u00f9 h\u1ee3p Gemini
Benchmark: 20 test cases so s\u00e1nh Score/Latency/Cost
C\u01a1 ch\u1ebf rollback v\u1ec1 GPT n\u1ebfu Gemini kh\u00f4ng \u1ed5n""", "category": "note", "pinned": True},
{"title": "v1.2 \u2014 \u0110\u1ed5i logic l\u1ea5y d\u1eef li\u1ec7u (th\u1eed nghi\u1ec7m)", "content": """Hybrid Search: vector + keyword thay v\u00ec vector-only
HNSW Index thay IVFFlat \u2192 recall t\u0103ng ~15%
Filters n\u00e2ng cao: l\u1ecdc gi\u00e1, m\u00e0u, size, danh m\u1ee5c tr\u01b0\u1edbc khi search
Caching layer (Redis): cache k\u1ebft qu\u1ea3 search ph\u1ed5 bi\u1ebfn, gi\u1ea3m latency""", "category": "note", "pinned": True},
{"title": "v2.0 \u2014 N\u00e2ng c\u1ea5p l\u1edbn (k\u1ebf ho\u1ea1ch)", "content": """Prompt Engineering v2: rewrite to\u00e0n b\u1ed9, multi-turn context, persona n\u00e2ng cao
Giao di\u1ec7n chatbot m\u1edbi: dark mode, product cards, quick replies
Multi-Agent: Sales Agent + Support Agent + Analytics Agent ph\u1ed1i h\u1ee3p
A/B Testing Engine: so s\u00e1nh prompt/model variants tr\u00ean live traffic
Customer Analytics Dashboard: ph\u00e2n t\u00edch h\u00e0nh vi kh\u00e1ch h\u00e0ng t\u1eeb chat data""", "category": "note", "pinned": True},
]
for note in notes:
body = json.dumps(note).encode("utf-8")
req = urllib.request.Request(BASE, data=body, headers={"Content-Type":"application/json"}, method="POST")
urllib.request.urlopen(req)
time.sleep(0.3)
print(f"Seeded {len(notes)} roadmap notes OK")
...@@ -36,7 +36,7 @@ from api.roadmap_flow_route import router as roadmap_flow_router ...@@ -36,7 +36,7 @@ from api.roadmap_flow_route import router as roadmap_flow_router
from api.experiment_log_route import router as experiment_log_router from api.experiment_log_route import router as experiment_log_router
from api.auth_route import router as auth_router from api.auth_route import router as auth_router
from api.product_desc_route import router as product_desc_router from api.product_desc_route import router as product_desc_router
from api.fashion_matches_route import router as fashion_matches_router from api.fashion_matches.router import router as fashion_matches_router
from api.bulk_ops_route import router as bulk_ops_router from api.bulk_ops_route import router as bulk_ops_router
from api.user_insight_route import router as user_insight_router from api.user_insight_route import router as user_insight_router
from api.reaction_simulator_route import router as reaction_simulator_router from api.reaction_simulator_route import router as reaction_simulator_router
...@@ -133,19 +133,22 @@ async def serve_static(file_path: str): ...@@ -133,19 +133,22 @@ async def serve_static(file_path: str):
@app.get("/home/{file_path:path}") @app.get("/home/{file_path:path}")
async def serve_home_static(file_path: str): async def serve_home_static(file_path: str):
"""Serve home dashboard explicitly under /home namespace.""" """Serve home dashboard explicitly under /home namespace."""
if not file_path or file_path == "index.html": if not file_path:
return RedirectResponse(url="/home/index.html") return RedirectResponse(url="/home/index.html")
if file_path == "index.html":
file_path = "index.html" # just ensuring normal flow down to checking file
# Check if the file exists under static/home # Check if the file exists under static/home
home_path = os.path.join(STATIC_DIR, "home", file_path) home_path = os.path.join(STATIC_DIR, "home", file_path)
if os.path.isfile(home_path): if os.path.isfile(home_path):
return FileResponse(home_path) return FileResponse(home_path)
# Fallback to standard static if not found (for common assets like css/js) # Fallback to standard static if not found (for common assets like css/js)
common_path = os.path.join(STATIC_DIR, file_path) common_path = os.path.join(STATIC_DIR, file_path)
if os.path.isfile(common_path): if os.path.isfile(common_path):
return FileResponse(common_path) return FileResponse(common_path)
return JSONResponse({"detail": "Not Found"}, status_code=404) return JSONResponse({"detail": "Not Found"}, status_code=404)
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
<link rel="stylesheet" href="/static/cache/cache.css"> <link rel="stylesheet" href="/static/cache/cache.css">
<style>
.topbar h1, .topbar .page-title { display: flex; align-items: center; gap: 10px; }
.page-title-icon { width: 22px; height: 22px; color: var(--gold, #B45309); flex-shrink: 0; }
</style>
</head> </head>
<body> <body>
...@@ -27,7 +31,7 @@ ...@@ -27,7 +31,7 @@
<div class="main"> <div class="main">
<div class="topbar"> <div class="topbar">
<div class="page-title">🗄️ Cache Manager</div> <div class="page-title"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="page-title-icon"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> Cache Manager</div>
<div style="margin-left:auto;font-size:10px;color:var(--m,#888)" id="topbar-meta">Loading...</div> <div style="margin-left:auto;font-size:10px;color:var(--m,#888)" id="topbar-meta">Loading...</div>
<button class="refresh-btn" onclick="loadAll()">🔄 Refresh</button> <button class="refresh-btn" onclick="loadAll()">🔄 Refresh</button>
</div> </div>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment