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

update

parent e8e02b1e
......@@ -289,6 +289,15 @@ class LeadStageGraph:
desc = (p.get("description") or "").strip()
if desc:
line += f"\n 📝 {desc[:200]}"
outfit = p.get("outfit_recommendations")
if outfit and isinstance(outfit, list):
outfit_lines = []
for o in outfit[:3]:
outfit_lines.append(f"{o.get('role', 'match')}: {o.get('match_product_name')} ({o.get('match_product_code')})")
if outfit_lines:
line += f"\n 👗 outfit_recommendations: {', '.join(outfit_lines)}"
lines.append(line)
return "\n".join(lines)
else:
......
......@@ -110,6 +110,11 @@ def resolve_product_name(raw_name: str) -> str:
"quần bò ống rộng" → "quần jean ống rộng"
"""
result = raw_name.lower().strip()
# Custom rule cho sơ mi cộc tay -> sơ mi ngắn tay
if "sơ mi" in result and "cộc tay" in result:
result = result.replace("cộc tay", "ngắn tay")
for synonym in _SORTED_SYNONYMS:
if synonym in result:
db_value = SYNONYM_TO_DB[synonym]
......
This diff is collapsed.
import logging
logger = logging.getLogger(__name__)
SIZE_MAPPING = {
"NU": {
"XS": "Cao 1m47-1m53, Nặng 38-43kg",
"S": "Cao 1m50-1m55, Nặng 41-46kg",
"M": "Cao 1m55-1m63, Nặng 47-52kg",
"L": "Cao 1m60-1m65, Nặng 53-58kg",
"XL": "Cao 1m62-1m66, Nặng 59-64kg",
},
"NAM": {
"S": "Cao 1m62-1m68, Nặng 57-62kg",
"M": "Cao 1m69-1m73, Nặng 63-67kg",
"L": "Cao 1m71-1m75, Nặng 68-72kg",
"XL": "Cao 1m73-1m77, Nặng 73-77kg",
"XXL": "Cao 1m75-1m79, Nặng 78-82kg",
},
"QUAN_NU": {
"26": "Vòng eo 65cm, Vòng mông 79-87cm",
"27": "Vòng eo 67.5cm, Vòng mông 81-89cm",
"28": "Vòng eo 70cm, Vòng mông 84-92cm",
"29": "Vòng eo 72.5cm, Vòng mông 86-94cm",
"30": "Vòng eo 75cm, Vòng mông 89-97cm",
},
"QUAN_NAM": {
"29": "Vòng eo 79.5cm, Vòng mông 96.5cm",
"30": "Vòng eo 82cm, Vòng mông 99cm",
"31": "Vòng eo 84.5cm, Vòng mông 101.5cm",
"32": "Vòng eo 87cm, Vòng mông 104cm",
"33": "Vòng eo 89cm, Vòng mông 106.5cm",
},
"TRE_EM": {
"90": "Dành cho bé 2Y, Cao 90cm, 10-13kg",
"92": "Dành cho bé 2Y, Cao 88-94cm, 10-13kg",
"98": "Dành cho bé 2-3Y, Cao 95-101cm, 13-15kg",
"100": "Dành cho bé 3-4Y, Cao 100cm, 14-17kg",
"104": "Dành cho bé 3-4Y, Cao 101-107cm, 15-18kg",
"110": "Dành cho bé 4-5Y, Cao 107-113cm, 18-23kg",
"116": "Dành cho bé 6Y, Cao 113-119cm, 22-25kg",
"120": "Dành cho bé 6-7Y, Cao 120cm, 24-29kg",
"122": "Dành cho bé 7Y, Cao 119-125cm, 25-28kg",
"128": "Dành cho bé 8Y, Cao 125-131cm, 28-32kg",
"130": "Dành cho bé 8Y, Cao 130cm, 29-33kg",
"134": "Dành cho bé 9Y, Cao 131-137cm, 32-36kg",
"140": "Dành cho bé 9-11Y, Cao 137-145cm, 33-39kg",
"150": "Dành cho bé 11-12Y, Cao 150cm, 39-45kg",
"152": "Dành cho bé 11-12Y, Cao 145-157cm, 39-46kg",
"160": "Dành cho bé 13-14Y, Cao 160cm, 45-52kg",
"164": "Dành cho bé 13-14Y, Cao 157-169cm, 46-55kg",
},
"UNISEX": {
"XXS": "Cao 1m55-1m63, Nặng 47-52kg",
"XS": "Cao 1m60-1m65, Nặng 53-58kg",
"S": "Cao 1m62-1m68, Nặng 57-62kg",
"M": "Cao 1m69-1m73, Nặng 63-67kg",
"L": "Cao 1m71-1m75, Nặng 68-72kg",
"XL": "Cao 1m73-1m77, Nặng 73-77kg",
"XXL": "Cao 1m75-1m79, Nặng 79-82kg",
}
}
def determine_table_key(gender: str, product_line: str) -> str:
"""Xác định bảng size phù hợp dựa trên giới tính và dòng sản phẩm."""
gender = (gender or "").lower().strip()
product_line = (product_line or "").lower().strip()
is_jeans_or_khaki = any(x in product_line for x in ["jean", "khaki", "kaki", "quần âu", "quần tây"])
is_bottom = "quần" in product_line
# 1. Trẻ em
if gender in ["boy", "girl", "kid", "bé trai", "bé gái", "be trai", "be gai", "trẻ em"]:
return "TRE_EM"
# 2. Unisex
if gender == "unisex":
return "UNISEX"
# 3. Quần size số (Jeans/Khaki/Âu) - Nếu không phải quần size chữ
if is_bottom and is_jeans_or_khaki:
if gender in ["women", "nu", "nữ", "female"]:
return "QUAN_NU"
elif gender in ["men", "nam", "male"]:
return "QUAN_NAM"
# 4. Áo / Quần chun (Dùng bảng chuẩn Nam / Nữ)
if gender in ["women", "nu", "nữ", "female"]:
return "NU"
elif gender in ["men", "nam", "male"]:
return "NAM"
return ""
def build_size_message(gender: str, product_line: str, sizes: list[str], description: str = "") -> str:
"""
Sinh ra message hướng dẫn chọn size chỉ chứa các size thực tế đang có (available_sizes).
Nếu mô tả sản phẩm có từ khóa form dáng (ôm, rộng) thì đính kèm mẹo.
"""
if not sizes:
return ""
table_key = determine_table_key(gender, product_line)
if not table_key:
return "" # Không xác định được bảng size
mapping = SIZE_MAPPING.get(table_key, {})
if not mapping:
return ""
# Tạo chuỗi thông tin cho các size khả dụng
size_lines = []
# Sort sizes logically if possible, but keep original order for now to avoid complexity with letters vs numbers
for s in sizes:
s_upper = str(s).strip().upper()
if s_upper in mapping:
size_lines.append(f"Size {s_upper} ({mapping[s_upper]})")
if not size_lines:
return "" # Các size đưa vào không có trong bảng (ví dụ freesize)
# Sinh phần mô tả đối tượng
obj_str = "Sản phẩm Nữ"
if table_key == "NAM": obj_str = "Sản phẩm Nam"
elif table_key == "TRE_EM": obj_str = "Sản phẩm Trẻ em"
elif table_key == "UNISEX": obj_str = "Sản phẩm Unisex (Phi giới tính)"
elif table_key == "QUAN_NU": obj_str = "Quần Nữ (Size số)"
elif table_key == "QUAN_NAM": obj_str = "Quần Nam (Size số)"
base_msg = f"{obj_str}. Các size ĐANG CÒN HÀNG: " + "; ".join(size_lines) + "."
# Tìm mẹo tư vấn (fit style hints) - Viết theo dạng mớm lời trực tiếp (Directive) cho AI
desc_lower = (description or "").lower()
tips = []
if any(x in desc_lower for x in ["dáng ôm", "slim fit", "slim-fit", "body", "ôm sát"]):
tips.append("🔥 Chỉ dẫn cho AI: Sản phẩm này là form ôm (slim-fit). Khi tư vấn size, hãy chèn thêm câu khuyên khách cực khéo: 'Dáng này thiết kế ôm body tôn dáng, nếu anh/chị thích mặc cử động thoải mái hoặc hay mặc lót bên trong thì mình nên nhích lên 1 size cho dễ chịu nhé ạ!'")
elif any(x in desc_lower for x in ["oversize", "thùng thình", "rộng rãi", "boxy"]):
tips.append("🔥 Chỉ dẫn cho AI: Sản phẩm này là form Oversize (rộng rãi). Hãy dặn dò khách thật tinh tế: 'Mẫu này hãng đã làm form rộng rãi sẵn rồi, anh/chị cứ dựa theo chiều cao cân nặng lấy ĐÚNG SIZE là lên dáng thụng cực chất, không cần phải cố tăng size đâu ạ. Còn nếu thích mặc gọn gàng thì mới lùi 1 size nhé!'")
elif any(x in desc_lower for x in ["regular", "vừa vặn"]):
tips.append("🔥 Chỉ dẫn cho AI: Sản phẩm có form Regular chuẩn. Hãy chốt size dứt khoát: 'Mẫu này form dáng vừa vặn cực chuẩn, anh/chị cứ chiếu theo đúng chiều cao cân nặng lấy chuẩn size là mặc vừa in, không cần đắn đo ạ!'")
if tips:
base_msg += "\n\n" + "\n".join(tips)
return base_msg
import sqlite3
import os
db_path = r'D:\cnf\chatbot-canifa-feedback\backend\database\canifa_ai_dump.sqlite'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT internal_ref_code FROM sr__test_db__magento_product_dimension_with_text_embedding WHERE magento_ref_code = '3TS26S005-SW001'")
print(cursor.fetchone())
conn.close()
# Size Column Migration
## Mục đích
Thêm cột `size` vào bảng `pg__dashboard_canifa__ultra_descriptions` để lưu **bảng size đã filter** (chỉ size thực tế có bán), giúp giảm token khi hiển thị trong chatbot.
## Files
- `migrations/002_add_size_column.sql` - Thêm cột `size`
- `populate_size_column.py` - Điền dữ liệu vào cột `size`
## Cách chạy
### Bước 1: Apply migration (thêm cột)
```bash
sqlite3 backend/database/canifa_ai_dump.sqlite < backend/database/migrations/002_add_size_column.sql
```
Hoặc mở DB bằng DB Browser for SQLite và chạy SQL:
```sql
ALTER TABLE pg__dashboard_canifa__ultra_descriptions ADD COLUMN size TEXT;
```
### Bước 2: Populate data
```bash
cd backend/database
python populate_size_column.py
```
Script sẽ:
1. Đọc `description_data->'huong_dan_size'` (bảng size đầy đủ với XS, S, M, L, XL)
2. Lọc chỉ giữ lại các size có trong `size_scale` (VD: "L, M, S, XL")
3. Ghi kết quả vào cột `size`
### Bước 3: Verify
```bash
sqlite3 backend/database/canifa_ai_dump.sqlite "SELECT id, size_scale, size FROM pg__dashboard_canifa__ultra_descriptions WHERE size IS NOT NULL LIMIT 1;"
```
Expected output:
- `size_scale`: "L, M, S, XL"
- `size`: Bảng markdown chỉ chứa S, M, L, XL (không có XS)
## Lợi ích
- Trước: `huong_dan_size` ~500 tokens (bảng đầy đủ 5-6 sizes)
- Sau: `size` ~100 tokens (chỉ sizes thực tế)
- Tiết kiệm ~400 tokens/sp × số sản phẩm hiển thị
## Notes
- Cột `size` được thêm vào SELECT trong `product_search_engine.py`
- Output `item["size_table"]` sẽ dùng cột `size` thay vì parse `huong_dan_size` mỗi lần
-- Migration: Add `size` column to ultra_descriptions
-- Purpose: Store filtered size table based on size_scale to reduce token usage
-- Date: 2026-05-05
-- Database: SQLite
-- 1. Add new column
ALTER TABLE pg__dashboard_canifa__ultra_descriptions ADD COLUMN size TEXT;
-- 2. Note: Use populate_size_column.py to fill this column
-- because SQLite lacks advanced string manipulation functions.
COMMIT;
"""
Populate `size` column in pg__dashboard_canifa__ultra_descriptions
with filtered size table based on size_scale.
"""
import json
import os
import sqlite3
import sys
sys.stdout.reconfigure(encoding='utf-8')
# Auto-detect DB path relative to this script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Try common DB filenames
possible_db_names = [
"canifa_ai_dump.sqlite",
"canifa_local.sqlite",
"canifa_dump.sqlite"
]
DB_PATH = None
for db_name in possible_db_names:
candidate = os.path.join(SCRIPT_DIR, db_name)
if os.path.exists(candidate):
DB_PATH = candidate
print(f"✅ Found DB: {DB_PATH}")
break
if not DB_PATH:
# Default to first option with debug
DB_PATH = os.path.join(SCRIPT_DIR, "canifa_ai_dump.sqlite")
print(f"⚠️ DB not auto-found. Will try: {DB_PATH}")
def parse_size_table_from_markdown(markdown: str) -> dict:
result = {}
lines = markdown.strip().split('\n')
for line in lines:
line = line.strip()
if line.startswith('|') and not line.startswith('|---'):
parts = [p.strip() for p in line.split('|')]
if len(parts) >= 4:
size = parts[1]
height = parts[2]
weight = parts[3]
result[size] = (height, weight)
return result
def build_filtered_table(size_table: dict, available_sizes: list) -> str:
if not size_table or not available_sizes:
return ""
lines = [
"| Size | Chiều cao (cm) | Cân nặng (kg) |",
"|------|----------------|---------------|"
]
for size in available_sizes:
size_upper = size.strip().upper()
for table_size, (height, weight) in size_table.items():
if table_size.upper() == size_upper:
lines.append(f"| {size} | {height} | {weight} |")
break
return '\n'.join(lines)
def main():
print(f"\n🔍 DB Path: {DB_PATH}")
print(f"📁 Script dir: {SCRIPT_DIR}")
print(f"📁 Current dir: {os.getcwd()}")
print(f"❌ Exists: {os.path.exists(DB_PATH)}\n")
if not os.path.exists(DB_PATH):
print("❌ Database not found!")
print("\nAvailable .sqlite files in script directory:")
for f in os.listdir(SCRIPT_DIR):
if f.endswith('.sqlite'):
print(f" - {f}")
print("\n👉 Please edit DB_PATH in this script to point to the correct file.")
return
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
# Ensure column exists
try:
cur.execute("ALTER TABLE pg__dashboard_canifa__ultra_descriptions ADD COLUMN size TEXT")
print("✅ Added 'size' column")
except sqlite3.OperationalError as e:
if "duplicate column" in str(e).lower():
print("ℹ️ Column 'size' already exists - will skip creation")
else:
print(f"❌ Error adding column: {e}")
return
# Fetch rows to process
cur.execute("""
SELECT id, description_data, size_scale
FROM pg__dashboard_canifa__ultra_descriptions
WHERE description_data IS NOT NULL
AND description_data != ''
AND size_scale IS NOT NULL
AND size_scale != ''
AND (size IS NULL OR size = '')
""")
rows = cur.fetchall()
total = len(rows)
print(f"📊 Found {total} rows to process")
if total == 0:
# Check if any rows have data already
cur.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ultra_descriptions")
total_rows = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ultra_descriptions WHERE size IS NOT NULL")
already_done = cur.fetchone()[0]
print(f"✅ No rows need processing. Total: {total_rows}, Already filled: {already_done}")
conn.close()
return
updated = 0
errors = 0
for row_id, desc_data_json, size_scale in rows:
try:
desc_data = json.loads(desc_data_json)
huong_dan_size = desc_data.get('huong_dan_size', '')
if not huong_dan_size:
continue
available_sizes = [s.strip() for s in size_scale.split('|') if s.strip()]
if not available_sizes:
continue
size_table = parse_size_table_from_markdown(huong_dan_size)
if not size_table:
continue
filtered_table = build_filtered_table(size_table, available_sizes)
if filtered_table:
cur.execute(
"UPDATE pg__dashboard_canifa__ultra_descriptions SET size = ? WHERE id = ?",
(filtered_table, row_id)
)
updated += 1
if updated % 100 == 0:
print(f" Progress: {updated}/{total}")
conn.commit()
except json.JSONDecodeError as e:
errors += 1
print(f" ⚠️ JSON parse error on row {row_id}: {e}")
except Exception as e:
errors += 1
print(f" ⚠️ Error on row {row_id}: {e}")
conn.commit()
# Verify
cur.execute("SELECT COUNT(*) FROM pg__dashboard_canifa__ultra_descriptions WHERE size IS NOT NULL")
count = cur.fetchone()[0]
print(f"\n✅ Done! Updated {updated} rows. Total with size: {count}. Errors: {errors}")
# Show sample
cur.execute("""
SELECT id, size_scale, size
FROM pg__dashboard_canifa__ultra_descriptions
WHERE size IS NOT NULL
LIMIT 1
""")
sample = cur.fetchone()
if sample:
print("\n📄 Sample output:")
print(f" ID: {sample[0]}")
print(f" size_scale: {sample[1]}")
print(f" size (filtered):")
print(" " + sample[2].replace('\n', '\n '))
conn.close()
if __name__ == "__main__":
main()
This diff is collapsed.
['/', '/health', '/static/{file_path}', '/home/{file_path}', '/api/agent/chat', '/api/agent/chat-dev', '/api/agent/user-insight', '/api/agent/system-prompt', '/api/prompt/refresh', '/api/agent/tool-prompts', '/api/agent/tool-prompts/{filename}', '/api/agent/n8n/products', '/api/agent/n8n/products/verify', '/api/agent/n8n/stock', '/api/agent/n8n/promotions', '/api/agent/n8n/stores', '/api/agent/n8n/knowledge', '/api/feedback', '/api/feedback/stats', '/api/feedback/list', '/api/feedback/process', '/api/feedback-agent/sync-langfuse', '/api/feedback-agent/analyze', '/api/feedback-agent/apply', '/api/diagram/chat', '/api/diagram/clear', '/api/sku-search/chat', '/api/sku-search/lookup/{sku}', '/api/insight/reset', '/api/check-history/{identity_key}', '/api/history/{identity_key}', '/api/history/archive', '/api/merge-history/mock', '/api/merge-history/guest/{device_id}', '/api/merge-history/preview', '/api/auth/login', '/api/auth/me', '/api/auth/refresh', '/api/auth/me/settings', '/api/auth/users', '/api/auth/users/{user_id}', '/api/auth/stats', '/api/auth/register', '/api/mock/login', '/api/mock/logout', '/api/mock/history', '/api/mock/new-conversation', '/api/mock/conversations', '/api/mock/chat', '/api/mock/links/{user_id}', '/api/products/overview', '/api/products/list', '/api/products/colors', '/api/products/filters', '/api/product/lookup', '/api/stock/check', '/api/canifa/search', '/api/product-desc/overview', '/api/product-desc/batch-status', '/api/product-desc/list', '/api/product-desc/generate', '/api/product-desc/filters', '/api/product-desc/saved/{internal_ref_code}', '/api/product-desc/saved/{internal_ref_code}/update', '/api/product-desc/saved/{internal_ref_code}/update-text', '/api/product-desc/saved/{internal_ref_code}/rewrite-field', '/api/product-desc/approve', '/api/product-desc/sync-material', '/api/product-desc/approve-all', '/api/product-desc/clean-missing-summary', '/api/product-desc/backfill-clean-description', '/api/product-desc/batch-generate', '/api/product-desc/batch-generate-all', '/api/product-desc/batch-status/{job_id}', '/api/product-desc/fields', '/api/product-desc/fields/{field_key}', '/api/product-desc/batch-generate-tags', '/api/product-desc/tags-batch-status', '/api/product-desc/n8n-generate', '/api/bulk/search', '/api/bulk/ai-search', '/api/bulk/fields', '/api/bulk/ai-edit', '/api/bulk/ai-edit-preview', '/api/bulk/ai-edit-status', '/api/bulk/update', '/api/bulk/batch-generate-tags', '/api/bulk/tags-batch-status', '/api/bulk/generate-tags-single', '/api/bulk/enrich-size-single', '/api/bulk/enrich-size-batch', '/api/bulk/size-stats', '/api/tags-direct/run', '/api/tags-direct/status', '/api/tags-direct/stop', '/api/tags-direct/single', '/api/tags-direct/count-untagged', '/api/fashion-matches/{code}', '/api/fashion-matches/{code}/update', '/api/fashion-matches/{code}/regen', '/api/fashion-matches/batch-regen', '/api/fashion-matches/batch', '/api/fashion-matches/batch/status', '/api/fashion-matches/rules/config', '/api/fashion-matches/rules/meta', '/api/fashion-matches/color-logic', '/api/fashion-matches/outfit-suggest', '/api/fashion-matches/score-test', '/api/fashion-matches/audit/tag-coverage', '/api/fashion-matches/rules/view', '/api/fashion-matches/simulator/search', '/api/fashion-matches/simulator/stream', '/api/outfit-matches/stats', '/api/outfit-matches/products', '/api/outfit-matches/{code}', '/api/store-search/chat', '/api/image-search/chat', '/api/v2/chat', '/api/sql-chat', '/api/sql-chat/status', '/api/sql-dashboard', '/api/ai-sql/trace', '/api/ai-sql/approve/{session_id}', '/api/ai-sql/sessions', '/api/ai-sql/sessions/{session_id}', '/api/ai-sql/conversations', '/api/ai-sql/conversations/{conv_id}', '/api/user-insights/all', '/api/user-insights/{identity_key}', '/api/dashboard/info', '/api/dashboard/links', '/api/dashboard/links/{link_id}', '/api/dashboard/links/{link_id}/pin', '/api/faqs', '/api/faqs/{faq_id}', '/api/faqs/stats', '/api/faqs/generate-variants', '/api/faqs/batch-generate', '/api/faqs/simulate-match', '/api/cache/stats', '/api/cache/keys', '/api/cache/clear', '/api/cache/clear-all', '/api/cache/get', '/api/live-monitor/bootstrap', '/api/live-monitor/stream', '/api/report-html', '/api/report-html/stream/{task_id}', '/reports', '/api/reports/conversations', '/api/reports/conversations/{conv_id}', '/api/reports/{report_id}', '/api/report-inline-edit', '/api/report-save-edit', '/api/report-followup', '/api/dashboard/experiment-links', '/api/dashboard/experiment-links/{link_id}', '/api/dashboard/experiment-links/{link_id}/versions', '/api/dashboard/experiment-links/{link_id}/versions/{idx}', '/api/dashboard/experiments', '/api/dashboard/experiments/{exp_id}', '/api/dashboard/experiments/{exp_id}/pin', '/api/dashboard/experiments/compare', '/api/dashboard/roadmap', '/api/dashboard/roadmap/{item_id}', '/api/dashboard/flow', '/api/dashboard/flow/{item_id}', '/api/social/webhook/facebook', '/api/social/webhook/tiktok', '/api/social/messages', '/api/social/messages/stats', '/api/social/messages/analyze', '/api/social/messages/clear', '/api/notifications', '/api/notifications/unread-count', '/api/notifications/{notification_id}/read', '/api/notifications/read-all', '/api/notifications/test', '/api/notifications/check-spike', '/api/content', '/api/content/stats', '/api/content/{content_id}', '/api/content/{content_id}/submit', '/api/content/{content_id}/approve', '/api/content/{content_id}/reject', '/api/content/{content_id}/changes', '/api/content/{content_id}/schedule', '/api/content/{content_id}/publish-now', '/api/templates', '/api/templates/categories', '/api/templates/search', '/api/templates/{template_id}', '/api/templates/feeds/categories', '/api/templates/feeds/{category}', '/api/queue', '/api/queue/{content_id}', '/api/queue/reorder', '/api/queue/calendar', '/api/queue/slots', '/api/queue/slots/reset', '/api/queue/retry-state', '/api/queue/publish-tick', '/api/media/upload', '/api/media', '/api/media/platforms', '/api/media/{asset_id}/info', '/api/media/{asset_id}', '/api/media/{asset_id}/thumb', '/api/media/{asset_id}/resize', '/api/mock/agent/chat', '/api/mock/db/search', '/api/mock/retriverdb', '/api/mock/retrieverdb', '/api/prompt-optimizer/run-test', '/api/prompt-optimizer/judge', '/api/prompt-optimizer/optimize', '/api/user-simulator/archetypes', '/api/user-simulator/generate-personas', '/api/user-simulator/generate-message', '/api/user-simulator/chat-with-bot', '/api/user-simulator/evaluate', '/api/user-simulator/synthesize', '/api/user-simulator/run-simulation', '/api/regression-test/run', '/api/stress-test/run', '/api/reaction-simulator/segments', '/api/reaction-simulator/campaign-types', '/api/reaction-simulator/simulate', '/api/reaction-simulator/simulate-mock', '/api/agent/chat-lead-flow', '/api/agent/lead-stage', '/api/lead/history', '/api/lead/conversations', '/api/lead/dashboard', '/api/lead-flow/simulate', '/api/limit/info', '/api/limit/reset']
\ No newline at end of file
{
"status": "success",
"ai_response": "Rất hợp đi làm cho nữ nè—mình gợi ý ngay 1 set “lịch sự - gọn dáng - dễ mặc mỗi ngày” từ 3 lựa chọn quần/váy cơ bản:\n\nOUTFIT A: Quần khaki nữ basic dáng suông (6BK25S002) + (bạn có thể phối thêm) áo sơ mi/blouse nữ cùng tông trung tính để lên form chuẩn công sở.\nOUTFIT B: Chân váy ngắn màu xanh nhạt (6KS25W002) + (bạn có thể phối thêm) áo sơ mi nữ dáng suông hoặc croptop che phủ gọn gàng (hợp đi làm).\nOUTFIT C: Quần jeans đen nữ (6BJ25C001) + (bạn có thể phối thêm) áo blouse/áo sơ mi nữ màu trơn để tổng thể tinh gọn.\n\nĐể mình chốt đúng set hợp nhất cho bạn, bạn cho mình biết chiều cao và cân nặng (mặc size S/M/L hay số quần bao nhiêu) nhé?",
"products": [],
"trace_id": "83c12c18a7cf6ec4d736e2df7d559278",
"lead_stage": {
"stage": 1,
"stage_name": "BROWSE",
"tone_directive": "Friendly",
"behavioral_hints": [
"Gợi ý combo set đi làm với nhiều lựa chọn (quần khaki/chân váy/jeans)."
]
},
"pipeline": [
{
"step": "user",
"label": "👤 User",
"content": "combo đi làm"
},
{
"step": "classifier",
"label": "🔧 Classifier → Early Exit",
"content": "Khách chỉ nói 'combo đi làm' và không có yêu cầu đính chính loại sản phẩm; đã có 3 SP được giới thiệu trong ngữ cảnh. Theo quy tắc recall, không search mới mà trả về lựa chọn từ SP đã show.",
"elapsed_ms": 1419
},
{
"step": "stylist",
"label": "💬 Stylist (Response + Insight)",
"content": "Rất hợp đi làm cho nữ nè—mình gợi ý ngay 1 set “lịch sự - gọn dáng - dễ mặc mỗi ngày” từ 3 lựa chọn quần/váy cơ bản:\n\nOUTFIT A: Quần khaki nữ basic dáng suông (6BK25S002) + (bạn có thể phối thêm) áo sơ mi/blouse nữ cùng tông trung tính để lên form chuẩn công sở.\nOUTFIT B: Chân váy ngắn màu xanh nhạt (6KS25W002) + (bạn có thể phối thêm) áo sơ mi nữ dáng suông hoặc croptop che phủ gọn gàng (hợp đi làm).\nOUTFIT C: Quần jeans đen nữ (6BJ25C001) + (bạn có thể phối thêm) áo blouse/áo sơ mi nữ màu trơn",
"elapsed_ms": 3633,
"raw_json": "{\n \"ai_response\": \"Rất hợp đi làm cho nữ nè—mình gợi ý ngay 1 set “lịch sự - gọn dáng - dễ mặc mỗi ngày” từ 3 lựa chọn quần/váy cơ bản:\\n\\nOUTFIT A: Quần khaki nữ basic dáng suông (6BK25S002) + (bạn có thể phối thêm) áo sơ mi/blouse nữ cùng tông trung tính để lên form chuẩn công sở.\\nOUTFIT B: Chân váy ngắn màu xanh nhạt (6KS25W002) + (bạn có thể phối thêm) áo sơ mi nữ dáng suông hoặc croptop che phủ gọn gàng (hợp đi làm).\\nOUTFIT C: Quần jeans đen nữ (6BJ25C001) + (bạn có thể phối thêm) áo blouse/áo sơ mi nữ màu trơn để tổng thể tinh gọn.\\n\\nĐể mình chốt đúng set hợp nhất cho bạn, bạn cho mình biết chiều cao và cân nặng (mặc size S/M/L hay số quần bao nhiêu) nhé?\",\n \"product_ids\": [\n \"6BK25S002\",\n \"6KS25W002\",\n \"6BJ25C001\"\n ],\n \"user_insight\": {\n \"USER\": \"Chưa rõ\",\n \"TARGET\": \"Nữ\",\n \"GOAL\": \"Đồ đi làm\",\n \"CONSTRAINS\": \"\",\n \"STAGE\": \"BROWSE\",\n \"STAGE_NUM\": 1,\n \"TONE\": \"Friendly\",\n \"BEHAVIORAL_HINTS\": [\n \"Gợi ý combo set đi làm với nhiều lựa chọn (quần khaki/chân váy/jeans).\"\n ],\n \"LATEST_PRODUCT_INTEREST\": \"Quần khaki nữ basic dáng suông (6BK25S002), Chân váy ngắn màu xanh nhạt (6KS25W002), Quần jeans đen nữ (6BJ25C001)\",\n \"LAST_ACTION\": \"Khách hỏi combo đi làm cho nữ\",\n \"SUMMARY_HISTORY\": \"Khách hỏi đồ đi làm cho nữ -> đã gợi ý 3 món: quần khaki (6BK25S002), chân váy xanh nhạt (6KS25W002), quần jeans đen (6BJ25C001).\"\n }\n}"
}
],
"timing": {
"classifier_ms": 1419,
"stylist_ms": 3633,
"tool_ms": 0,
"total_ms": 5062
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3"/>
<text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="20" font-weight="700">Canifa AI Platform Architecture</text>
<!-- Client Layer -->
<text x="50" y="125" fill="#6a6a6a" font-size="14" font-weight="600">Client</text>
<rect x="380" y="80" width="200" height="80" rx="12" ry="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="120" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Browser (MFE)</text>
<text x="480" y="140" text-anchor="middle" fill="#6a6a6a" font-size="12">Static HTML/JS/CSS</text>
<!-- FastAPI Layer -->
<text x="50" y="285" fill="#6a6a6a" font-size="14" font-weight="600">Gateway</text>
<rect x="380" y="240" width="200" height="90" rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="275" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">FastAPI (Lifespan)</text>
<text x="480" y="295" text-anchor="middle" fill="#6a6a6a" font-size="12">Orchestrator &amp; Routing</text>
<text x="480" y="315" text-anchor="middle" fill="#6a6a6a" font-size="12">Graceful Startup/Shutdown</text>
<!-- Data/Infra Layer -->
<text x="50" y="485" fill="#6a6a6a" font-size="14" font-weight="600">Storage / Bus</text>
<!-- EventBus -->
<rect x="100" y="440" width="200" height="80" rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="200" y="480" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">EventBus</text>
<text x="200" y="500" text-anchor="middle" fill="#6a6a6a" font-size="12">FastStream (Pub/Sub)</text>
<!-- Redis -->
<rect x="380" y="440" width="200" height="80" rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="480" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Redis Cache</text>
<text x="480" y="500" text-anchor="middle" fill="#6a6a6a" font-size="12">State &amp; TTL Management</text>
<!-- Postgres -->
<rect x="660" y="440" width="200" height="80" rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="760" y="480" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Postgres / StarRocks</text>
<text x="760" y="500" text-anchor="middle" fill="#6a6a6a" font-size="12">Relational &amp; OLAP Data</text>
<!-- Arrows -->
<!-- Client -> FastAPI -->
<line x1="480" y1="160" x2="480" y2="240" stroke="#5a5a5a" stroke-width="2.5" marker-end="url(#arrow-claude)"/>
<rect x="445" y="190" width="70" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="480" y="205" text-anchor="middle" fill="#5a5a5a" font-size="13">HTTP / WS</text>
<!-- FastAPI -> EventBus (ortho) -->
<path d="M 380,285 L 200,285 L 200,440" fill="none" stroke="#5a5a5a" stroke-width="2.5" marker-end="url(#arrow-claude)"/>
<rect x="165" y="340" width="70" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="200" y="355" text-anchor="middle" fill="#5a5a5a" font-size="13">Pub/Sub</text>
<!-- FastAPI -> Redis -->
<line x1="480" y1="330" x2="480" y2="440" stroke="#5a5a5a" stroke-width="2.5" marker-end="url(#arrow-claude)"/>
<rect x="445" y="375" width="70" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="480" y="390" text-anchor="middle" fill="#5a5a5a" font-size="13">Init / Cache</text>
<!-- FastAPI -> Postgres (ortho) -->
<path d="M 580,285 L 760,285 L 760,440" fill="none" stroke="#5a5a5a" stroke-width="2.5" marker-end="url(#arrow-claude)"/>
<rect x="725" y="340" width="70" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="760" y="355" text-anchor="middle" fill="#5a5a5a" font-size="13">db_pool</text>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<rect width="960" height="700" fill="#f8f6f3"/>
<text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="22" font-weight="700">Canifa AI Platform Architecture</text>
<text x="40" y="120" fill="#6a6a6a" font-size="14" font-weight="600">Client</text>
<text x="40" y="240" fill="#6a6a6a" font-size="14" font-weight="600">Interface</text>
<text x="40" y="360" fill="#6a6a6a" font-size="14" font-weight="600">Gateway</text>
<text x="40" y="480" fill="#6a6a6a" font-size="14" font-weight="600">Module</text>
<text x="40" y="600" fill="#6a6a6a" font-size="14" font-weight="600">Data</text>
<rect x="380" y="80" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="115" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Web Browser</text>
<text x="480" y="135" text-anchor="middle" fill="#6a6a6a" font-size="13">Static Entry Points</text>
<rect x="380" y="200" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="235" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Micro-Frontends</text>
<text x="480" y="255" text-anchor="middle" fill="#6a6a6a" font-size="13">Independent Namespaces</text>
<rect x="380" y="320" width="200" height="80" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="355" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">FastAPI Gateway</text>
<text x="480" y="375" text-anchor="middle" fill="#6a6a6a" font-size="13">api_router &amp; Middleware</text>
<rect x="380" y="440" width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="475" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Business Modules</text>
<text x="480" y="495" text-anchor="middle" fill="#6a6a6a" font-size="13">36+ Agents &amp; Event Bus</text>
<rect x="180" y="560" width="180" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="270" y="595" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Postgres DB</text>
<text x="270" y="615" text-anchor="middle" fill="#6a6a6a" font-size="13">db_pool management</text>
<rect x="390" y="560" width="180" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="595" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Redis Cache</text>
<text x="480" y="615" text-anchor="middle" fill="#6a6a6a" font-size="13">redis_cache module</text>
<rect x="600" y="560" width="180" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="690" y="595" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">OLAP (StarRocks)</text>
<text x="690" y="615" text-anchor="middle" fill="#6a6a6a" font-size="13">Analytic Engine</text>
<line x1="480" y1="160" x2="480" y2="200" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<line x1="480" y1="280" x2="480" y2="320" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<line x1="480" y1="400" x2="480" y2="440" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<path d="M 480 520 L 480 540 L 270 540 L 270 560" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<path d="M 480 520 L 480 560" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<path d="M 480 520 L 480 540 L 690 540 L 690 560" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "01-architecture",
"title": "Platform Architecture Overview",
"description": "Cái nhìn tổng quan về kiến trúc 5 lớp của Canifa AI Platform, từ giao diện người dùng đến các module API và hạ tầng dữ liệu.",
"diagram": "diagrams/platform-architecture.svg",
"diagram": "data/01-architecture/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 650" width="960" height="650">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
.title { font-size: 22px; font-weight: 700; fill: #1a1a1a; }
.node-label { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.node-detail { font-size: 13px; fill: #6a6a6a; }
.arrow-label { font-size: 13px; font-weight: 600; fill: #5a5a5a; }
.layer-label { font-size: 14px; font-weight: 600; fill: #6a6a6a; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<rect width="960" height="650" fill="#f8f6f3"/>
<text x="480" y="45" text-anchor="middle" class="title">Database Layer &amp; SQLite Mock Architecture</text>
<!-- Layer Labels -->
<text x="50" y="140" class="layer-label">Application</text>
<text x="50" y="270" class="layer-label">Routing</text>
<text x="50" y="420" class="layer-label">Translation/Pool</text>
<text x="50" y="570" class="layer-label">Data Store</text>
<!-- --- Nodes --- -->
<!-- App Node -->
<rect x="380" y="100" width="200" height="80" rx="12" ry="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="135" text-anchor="middle" class="node-label">App (FastAPI / Helper)</text>
<text x="480" y="155" text-anchor="middle" class="node-detail">Sync Helpers &amp; Async API</text>
<!-- Config Check Node -->
<rect x="380" y="230" width="200" height="80" rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="265" text-anchor="middle" class="node-label">USE_LOCAL_SQLITE?</text>
<text x="480" y="285" text-anchor="middle" class="node-detail">Environment Routing</text>
<!-- SQLite Path: Translator -->
<rect x="180" y="380" width="200" height="90" rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="280" y="415" text-anchor="middle" class="node-label">SQLite Mock Translator</text>
<text x="280" y="435" text-anchor="middle" class="node-detail">Regex-based SQL Rewrite</text>
<text x="280" y="452" text-anchor="middle" class="node-detail">(sqlite_mock.py)</text>
<!-- SQLite Path: DB -->
<rect x="180" y="530" width="200" height="80" rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="280" y="565" text-anchor="middle" class="node-label">Local SQLite</text>
<text x="280" y="585" text-anchor="middle" class="node-detail">Development Mock DB</text>
<!-- Postgres Path: Pool -->
<rect x="580" y="380" width="200" height="90" rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="680" y="415" text-anchor="middle" class="node-label">Postgres Pool</text>
<text x="680" y="435" text-anchor="middle" class="node-detail">psycopg_pool / asyncpg</text>
<text x="680" y="452" text-anchor="middle" class="node-detail">(CanifaDbPool)</text>
<!-- Postgres Path: DB -->
<rect x="580" y="530" width="200" height="80" rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="680" y="565" text-anchor="middle" class="node-label">PostgreSQL DB</text>
<text x="680" y="585" text-anchor="middle" class="node-detail">Production Database</text>
<!-- --- Arrows --- -->
<!-- App -> Config Check -->
<line x1="480" y1="180" x2="480" y2="230" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<rect x="440" y="195" width="80" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="480" y="210" text-anchor="middle" class="arrow-label">Query Request</text>
<!-- Config Check -> True Path (SQLite) -->
<path d="M 380 270 Q 280 270 280 380" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<rect x="250" y="280" width="60" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="280" y="295" text-anchor="middle" class="arrow-label" fill="#059669">True</text>
<!-- Config Check -> False Path (Postgres) -->
<path d="M 580 270 Q 680 270 680 380" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<rect x="650" y="280" width="60" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="680" y="295" text-anchor="middle" class="arrow-label" fill="#dc2626">False</text>
<!-- SQLite Translator -> SQLite DB -->
<line x1="280" y1="470" x2="280" y2="530" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<rect x="210" y="490" width="140" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="280" y="505" text-anchor="middle" class="arrow-label">Translated SQL</text>
<!-- Postgres Pool -> Postgres DB -->
<line x1="680" y1="470" x2="680" y2="530" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<rect x="620" y="490" width="120" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="680" y="505" text-anchor="middle" class="arrow-label">Raw Postgres SQL</text>
<!-- Legend -->
<rect x="700" y="30" width="220" height="85" rx="8" ry="8" fill="#ffffff" stroke="#4a4a4a" stroke-width="1.5" opacity="0.9"/>
<text x="715" y="55" class="node-label" font-size="14">Legend</text>
<rect x="715" y="65" width="12" height="12" fill="#a8c5e6" stroke="#4a4a4a"/>
<text x="735" y="75" class="node-detail" font-size="12">Source / Client</text>
<rect x="715" y="82" width="12" height="12" fill="#9dd4c7" stroke="#4a4a4a"/>
<text x="735" y="92" class="node-detail" font-size="12">Process / Logic</text>
<rect x="715" y="99" width="12" height="12" fill="#f4e4c1" stroke="#4a4a4a"/>
<text x="735" y="109" class="node-detail" font-size="12">Infrastructure / Pool</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3"/>
<text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="22" font-weight="700">Database Connection &amp; Routing Logic</text>
<text x="40" y="100" fill="#6a6a6a" font-size="14" font-weight="600">Application</text>
<text x="40" y="240" fill="#6a6a6a" font-size="14" font-weight="600">Router</text>
<text x="40" y="420" fill="#6a6a6a" font-size="14" font-weight="600">Storage Layer</text>
<rect x="380" y="70" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="105" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">App / Helper</text>
<text x="480" y="125" text-anchor="middle" fill="#6a6a6a" font-size="13">FastAPI or Class Helpers</text>
<rect x="380" y="200" width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="235" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Connection Switch</text>
<text x="480" y="255" text-anchor="middle" fill="#6a6a6a" font-size="13">Check USE_LOCAL_SQLITE</text>
<rect x="180" y="380" width="200" height="100" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="280" y="415" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">SQLite Mock</text>
<text x="280" y="435" text-anchor="middle" fill="#6a6a6a" font-size="13">SQL Translation</text>
<text x="280" y="455" text-anchor="middle" fill="#6a6a6a" font-size="13">sqlite_mock.py</text>
<rect x="580" y="380" width="200" height="100" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="680" y="415" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Postgres Pool</text>
<text x="680" y="435" text-anchor="middle" fill="#6a6a6a" font-size="13">psycopg_pool / asyncpg</text>
<text x="680" y="455" text-anchor="middle" fill="#6a6a6a" font-size="13">CanifaDbPool</text>
<line x1="480" y1="150" x2="480" y2="200" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="490" y="180" fill="#5a5a5a" font-size="13">Connect/Query</text>
<path d="M 380 240 L 280 240 L 280 380" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="200" y="310" fill="#5a5a5a" font-size="13">True (Local)</text>
<path d="M 580 240 L 680 240 L 680 380" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="700" y="310" fill="#5a5a5a" font-size="13">False (Prod)</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "01b-database",
"title": "Database Layer & SQLite Mock",
"description": "Chi tiết về cách hệ thống xử lý dữ liệu đồng bộ và bất đồng bộ, cùng cơ chế SQLite Mock để giả lập Postgres trong môi trường phát triển.",
"diagram": "diagrams/database-layer.svg",
"diagram": "data/01b-database/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3"/>
<text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="22" font-weight="700">Authentication &amp; RBAC Flow</text>
<rect x="100" y="100" width="200" height="400" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1" stroke-dasharray="5,5"/>
<text x="200" y="125" text-anchor="middle" fill="#6a6a6a" font-size="14" font-weight="600">Browser / Frontend</text>
<rect x="380" y="100" width="200" height="400" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1" stroke-dasharray="5,5"/>
<text x="480" y="125" text-anchor="middle" fill="#6a6a6a" font-size="14" font-weight="600">FastAPI Backend</text>
<rect x="660" y="100" width="200" height="400" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1" stroke-dasharray="5,5"/>
<text x="760" y="125" text-anchor="middle" fill="#6a6a6a" font-size="14" font-weight="600">PostgreSQL DB</text>
<rect x="120" y="160" width="160" height="60" rx="10" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="200" y="195" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">auth.js Guard</text>
<rect x="400" y="160" width="160" height="60" rx="10" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="480" y="195" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">POST /api/auth/login</text>
<rect x="680" y="160" width="160" height="60" rx="10" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="760" y="195" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">admin_users table</text>
<rect x="400" y="300" width="160" height="60" rx="10" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="480" y="335" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">get_current_user</text>
<rect x="120" y="300" width="160" height="60" rx="10" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="200" y="335" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">localStorage</text>
<line x1="280" y1="180" x2="400" y2="180" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>
<text x="340" y="175" text-anchor="middle" fill="#5a5a5a" font-size="11">Credentials</text>
<line x1="560" y1="190" x2="680" y2="190" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>
<text x="620" y="185" text-anchor="middle" fill="#5a5a5a" font-size="11">Verify</text>
<line x1="400" y1="210" x2="280" y2="210" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>
<text x="340" y="205" text-anchor="middle" fill="#5a5a5a" font-size="11">JWT Token</text>
<line x1="200" y1="220" x2="200" y2="300" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>
<text x="140" y="260" text-anchor="middle" fill="#5a5a5a" font-size="11">Store Token</text>
<line x1="280" y1="330" x2="400" y2="330" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>
<text x="340" y="325" text-anchor="middle" fill="#5a5a5a" font-size="11">Auth Header</text>
<line x1="560" y1="330" x2="680" y2="190" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>
<text x="630" y="270" text-anchor="middle" fill="#5a5a5a" font-size="11" transform="rotate(-30 630 270)">Check Role</text>
</svg>
\ No newline at end of file
......@@ -2,30 +2,6 @@
Kiến trúc cốt lõi sử dụng `StateGraph` từ LangGraph với 2 node chính. Không sử dụng ReAct loop truyền thống để giảm độ trễ và tránh ảo giác vòng lặp.
```mermaid
sequenceDiagram
participant U as User
participant C as Classifier Node
participant T as Tool Registry (RAG/DB)
participant S as Stylist Node
participant DB as StarRocks / Postgres
U->>C: Nhập câu hỏi (Query)
Note over C: Tiêm History (10 turns) + Insight cũ
C->>C: Sinh Structured Output (Tool + Args)
alt Không cần gọi Tool (Early Exit)
C-->>U: AI Response (Chào hỏi / Tâm sự)
else Cần gọi Tool
C->>T: Execute Tool (ví dụ: lead_search_tool)
T->>DB: Query DB / Vector Search
DB-->>T: JSON Kết quả
T-->>S: Tool Result + Context
Note over S: Tiêm STYLIST_SYSTEM_PROMPT
S->>S: Sinh AI Response + InsightJSON mới
S-->>U: Trả lời kèm SP
end
```
**Các bước cụ thể:**
1. **Tiếp nhận:** State `LeadStageState` lưu trữ `messages`, `user_insight`, `tool_result`, và `diagnostics`.
2. **Định tuyến (Classifier):** AI quyết định gọi công cụ như `lead_search_tool`, `canifa_knowledge_search` (truy xuất RAG), hoặc `check_is_stock`.
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; }
.title { font-size: 24px; font-weight: 700; fill: #1a1a1a; }
.node-title { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.node-sub { font-size: 14px; font-weight: 400; fill: #1a1a1a; }
.layer-label { font-size: 14px; font-weight: 600; fill: #6a6a6a; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#00000010"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3"/>
<text x="480" y="50" text-anchor="middle" class="title">Chatbot Core (LangGraph) Architecture</text>
<!-- Layers -->
<rect x="50" y="100" width="860" height="120" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2" stroke-dasharray="8,4"/>
<text x="70" y="130" class="layer-label">Orchestration Layer</text>
<rect x="50" y="240" width="860" height="200" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2" stroke-dasharray="8,4"/>
<text x="70" y="270" class="layer-label">Agent Reasoning (DAG)</text>
<rect x="50" y="460" width="860" height="100" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2" stroke-dasharray="8,4"/>
<text x="70" y="490" class="layer-label">External Tools &amp; Data</text>
<!-- Nodes -->
<rect x="390" y="130" width="180" height="60" rx="12" fill="#ffffff" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="480" y="165" text-anchor="middle" class="node-title">FastAPI Controller</text>
<rect x="150" y="290" width="200" height="100" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="250" y="325" text-anchor="middle" class="node-title">Classifier Node</text>
<text x="250" y="345" text-anchor="middle" class="node-sub">• Structured Output</text>
<text x="250" y="365" text-anchor="middle" class="node-sub">• Intent Routing</text>
<rect x="610" y="290" width="200" height="100" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="710" y="325" text-anchor="middle" class="node-title">Stylist Node</text>
<text x="710" y="345" text-anchor="middle" class="node-sub">• Response Gen</text>
<text x="710" y="365" text-anchor="middle" class="node-sub">• Insight Update</text>
<rect x="380" y="480" width="200" height="60" rx="12" fill="#ffffff" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow)"/>
<text x="480" y="515" text-anchor="middle" class="node-title">StarRocks RAG / SQL</text>
<!-- Arrows -->
<line x1="480" y1="190" x2="250" y2="290" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<line x1="350" y1="340" x2="610" y2="340" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<line x1="710" y1="390" x2="480" y2="480" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<line x1="480" y1="480" x2="250" y2="390" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700" width="960" height="700">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
.title { font-size: 24px; font-weight: 700; fill: #1a1a1a; }
.label { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.detail { font-size: 13px; fill: #6a6a6a; }
.arrow-text { font-size: 13px; fill: #5a5a5a; font-weight: 500; }
.layer-label { font-size: 14px; font-weight: 600; fill: #6a6a6a; opacity: 0.8; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
<path d="M0,0 L10,5 L0,10 Z" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<!-- Warm cream background -->
<rect width="960" height="700" fill="#f8f6f3"/>
<!-- Title -->
<text x="480" y="45" text-anchor="middle" class="title">Chatbot Core &amp; n8n Pipeline Architecture</text>
<!-- Layer Labels -->
<text x="50" y="110" class="layer-label">INPUT</text>
<text x="50" y="270" class="layer-label">ORCHESTRATION</text>
<text x="50" y="430" class="layer-label">TOOLS &amp; REASONING</text>
<text x="50" y="590" class="layer-label">DATA</text>
<!-- User Node -->
<rect x="380" y="80" width="200" height="60" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="105" text-anchor="middle" class="label">User</text>
<text x="480" y="125" text-anchor="middle" class="detail">Customer Query (Text)</text>
<!-- Classifier Node -->
<rect x="380" y="240" width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="265" text-anchor="middle" class="label">Classifier (AI 1)</text>
<text x="480" y="285" text-anchor="middle" class="detail">LangGraph State Node</text>
<text x="480" y="305" text-anchor="middle" class="detail">Structured Tool Selection</text>
<!-- Tool Execution Node -->
<rect x="230" y="400" width="220" height="80" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="340" y="425" text-anchor="middle" class="label">Tool Execution</text>
<text x="340" y="445" text-anchor="middle" class="detail">RAG Search / Stock Check</text>
<text x="340" y="465" text-anchor="middle" class="detail">Python / n8n Integrations</text>
<!-- Stylist Node -->
<rect x="530" y="400" width="220" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="640" y="425" text-anchor="middle" class="label">Stylist (AI 2)</text>
<text x="640" y="445" text-anchor="middle" class="detail">Response Generation</text>
<text x="640" y="465" text-anchor="middle" class="detail">Insight Extraction</text>
<!-- Database Node -->
<rect x="230" y="560" width="220" height="60" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="340" y="585" text-anchor="middle" class="label">StarRocks / Postgres</text>
<text x="340" y="605" text-anchor="middle" class="detail">Knowledge Base &amp; Logs</text>
<!-- Flow Arrows -->
<!-- User -> Classifier -->
<path d="M480,140 L480,230" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none"/>
<text x="485" y="195" class="arrow-text">Query</text>
<!-- Classifier -> Tool (curved) -->
<path d="M380,280 C280,280 280,350 340,390" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none"/>
<text x="240" y="340" class="arrow-text">Execute Tool</text>
<!-- Tool -> Stylist -->
<path d="M450,440 L520,440" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none"/>
<text x="485" y="430" text-anchor="middle" class="arrow-text">Context</text>
<!-- Stylist -> User (long return) -->
<path d="M640,400 C640,110 590,110 590,110" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none"/>
<text x="645" y="240" class="arrow-text">Response + SP</text>
<!-- Early Exit (Dashed) -->
<path d="M580,280 C680,280 680,150 590,120" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none" stroke-dasharray="5,3"/>
<text x="685" y="200" class="arrow-text">Early Exit</text>
<!-- Tool -> DB -->
<path d="M320,480 L320,550" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none"/>
<text x="250" y="525" class="arrow-text">DB Query</text>
<!-- DB -> Tool -->
<path d="M360,560 L360,490" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)" fill="none"/>
<text x="365" y="525" class="arrow-text">Results</text>
<!-- Legend -->
<rect x="720" y="580" width="220" height="100" rx="8" fill="#ffffff" stroke="#4a4a4a" stroke-width="1.5"/>
<text x="735" y="605" class="label" style="font-size:14px">Legend</text>
<line x1="735" y1="630" x2="765" y2="630" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="775" y="635" class="detail">Primary Data Flow</text>
<line x1="735" y1="655" x2="765" y2="655" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
<text x="775" y="660" class="detail">Direct AI Response</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "02-chatbot-core",
"title": "Chatbot Core & n8n Pipeline",
"description": "Khám phá luồng xử lý tin nhắn của Fashion Q&A Agent, từ lúc nhận query đến khi gọi các tool n8n và trả về kết quả cho người dùng.",
"diagram": "diagrams/chatbot-pipeline.svg",
"diagram": "chatbot_graph_arch.svg",
"sections": [
{
"id": "sec0",
......
......@@ -3,36 +3,4 @@ Quy trình update và sử dụng prompt diễn ra như sau:
1. **UI Edit:** Admin sửa prompt trên giao diện và gọi API (ví dụ: `POST /api/agent/system-prompt`).
2. **File Override & Cache Invalidation:** Hệ thống ghi đè file `.txt` tương ứng và gọi hàm `force_refresh_prompts()` để xóa in-memory cache trong `prompt_utils.py`.
3. **MD5 Hash Check:** Ở request tiếp theo, `graph.py` lấy template mới. Nó tính mã băm MD5 của template. Nếu khác với `_cached_prompt_hash` trước đó, chuỗi (chain) LangChain sẽ được compile lại.
4. **LLM Factory Execution:** Chain sử dụng instance LLM được lấy từ `LLMFactory` (đã được cache để không khởi tạo lại network client liên tục) để thực thi.
### Sequence Diagram
```mermaid
sequenceDiagram
participant Admin
participant API (prompt_route.py)
participant utils (prompt_utils.py)
participant FileSystem
participant graph.py
participant LLMFactory
Admin->>API (prompt_route.py): POST /api/agent/system-prompt
API (prompt_route.py)->>FileSystem: Write to system_prompt.txt
API (prompt_route.py)->>utils (prompt_utils.py): force_refresh_prompts()
utils (prompt_utils.py)-->>API (prompt_route.py): Clear in-memory caches
API (prompt_route.py)-->>Admin: Success Response
Note over graph.py: On next user chat message
graph.py->>utils (prompt_utils.py): get_system_prompt_template()
utils (prompt_utils.py)->>FileSystem: Read system_prompt.txt (cache miss)
utils (prompt_utils.py)-->>graph.py: Return injected template
graph.py->>graph.py: Compute MD5 of prompt
alt MD5 Hash Changed
graph.py->>LLMFactory: get_model()
LLMFactory-->>graph.py: Cached LLM Instance
graph.py->>graph.py: Rebuild ChatPromptTemplate & Chain
end
graph.py->>LLMFactory (LLM): invoke()
```
\ No newline at end of file
4. **LLM Factory Execution:** Chain sử dụng instance LLM được lấy từ `LLMFactory` (đã được cache để không khởi tạo lại network client liên tục) để thực thi.
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "03-prompt-system",
"title": "Prompt System & LLM Factory",
"description": "Hướng dẫn chi tiết quản lý System Prompt và Tool Prompts, cơ chế MD5 Caching, xử lý LangChain variables và kiến trúc LLM Factory.",
"diagram": "diagrams/prompt-management.svg",
"diagram": "prompt_cache_arch.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
.title { font-size: 22px; font-weight: 700; fill: #1a1a1a; }
.node-label { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.node-detail { font-size: 13px; fill: #5a5a5a; }
.arrow-label { font-size: 13px; fill: #5a5a5a; font-weight: 500; }
.layer-label { font-size: 14px; font-weight: 600; fill: #6a6a6a; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
<path d="M0,0 L10,5 L0,10 Z" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<!-- Warm cream background -->
<rect width="960" height="600" fill="#f8f6f3"/>
<!-- Title -->
<text x="480" y="50" text-anchor="middle" class="title">Prompt System Caching Architecture</text>
<!-- Layer Labels -->
<text x="40" y="155" class="layer-label">Interaction</text>
<text x="40" y="275" class="layer-label">Cache Logic</text>
<text x="40" y="395" class="layer-label">Compilation</text>
<text x="40" y="515" class="layer-label">Execution</text>
<!-- Nodes -->
<!-- UI Edit -->
<rect x="360" y="115" width="240" height="80" rx="12" ry="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="480" y="145" text-anchor="middle" class="node-label">UI Edit (Admin)</text>
<text x="480" y="165" text-anchor="middle" class="node-detail">POST /api/agent/system-prompt</text>
<text x="480" y="182" text-anchor="middle" class="node-detail">• Updates system_prompt.txt</text>
<!-- Cache Invalidation -->
<rect x="360" y="235" width="240" height="80" rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="480" y="265" text-anchor="middle" class="node-label">Cache Invalidation</text>
<text x="480" y="285" text-anchor="middle" class="node-detail">force_refresh_prompts()</text>
<text x="480" y="302" text-anchor="middle" class="node-detail">• Clears in-memory cache</text>
<!-- prompt_utils (MD5 Hash Check) -->
<rect x="360" y="355" width="240" height="80" rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="480" y="385" text-anchor="middle" class="node-label">prompt_utils (MD5 Check)</text>
<text x="480" y="405" text-anchor="middle" class="node-detail">_cached_prompt_hash validation</text>
<text x="480" y="422" text-anchor="middle" class="node-detail">• Recompiles chain if changed</text>
<!-- LLM Factory -->
<rect x="360" y="475" width="240" height="80" rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>
<text x="480" y="505" text-anchor="middle" class="node-label">LLM Factory</text>
<text x="480" y="525" text-anchor="middle" class="node-detail">Cached Model Instance</text>
<text x="480" y="542" text-anchor="middle" class="node-detail">• Single client initialization</text>
<!-- Arrows -->
<!-- UI Edit to Invalidation -->
<line x1="480" y1="195" x2="480" y2="225" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="490" y="215" class="arrow-label">Refresh Trigger</text>
<!-- Invalidation to MD5 Check -->
<line x1="480" y1="315" x2="480" y2="345" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
<text x="490" y="335" class="arrow-label">On Next Read</text>
<!-- MD5 Check to LLM Factory -->
<line x1="480" y1="435" x2="480" y2="465" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="490" y="455" class="arrow-label">Invoke</text>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 720" width="960" height="720">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
.title { font-size: 24px; font-weight: 700; fill: #1a1a1a; }
.layer-label { font-size: 14px; font-weight: 600; fill: #6a6a6a; }
.node-title { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.node-detail { font-size: 13px; fill: #5a5a5a; }
.arrow-label { font-size: 12px; fill: #5a5a5a; font-weight: 500; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<!-- Warm cream background -->
<rect width="960" height="720" fill="#f8f6f3"/>
<!-- Title -->
<text x="480" y="50" text-anchor="middle" class="title">Feedback &amp; Learning Loop Architecture</text>
<!-- Layer Labels -->
<text x="40" y="140" class="layer-label">Collection</text>
<text x="40" y="300" class="layer-label">Storage</text>
<text x="40" y="480" class="layer-label">Learning</text>
<text x="40" y="640" class="layer-label">Optimization</text>
<!-- Nodes -->
<!-- User Feedback UI -->
<g transform="translate(380, 100)" filter="url(#shadow-soft)">
<rect width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2"/>
<text x="100" y="35" text-anchor="middle" class="node-title">User Interaction</text>
<text x="100" y="55" text-anchor="middle" class="node-detail">Rating (Like/Dislike)</text>
<text x="100" y="70" text-anchor="middle" class="node-detail">&amp; User Comments</text>
</g>
<!-- Feedback Pipeline -->
<g transform="translate(380, 260)" filter="url(#shadow-soft)">
<rect width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2"/>
<text x="100" y="35" text-anchor="middle" class="node-title">Feedback Pipeline</text>
<text x="100" y="55" text-anchor="middle" class="node-detail">FastAPI BackgroundTasks</text>
<text x="100" y="70" text-anchor="middle" class="node-detail">Async Data Processing</text>
</g>
<!-- Langfuse Store -->
<g transform="translate(180, 420)" filter="url(#shadow-soft)">
<rect width="200" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2"/>
<text x="100" y="35" text-anchor="middle" class="node-title">Langfuse Store</text>
<text x="100" y="55" text-anchor="middle" class="node-detail">Trace Data &amp; Scores</text>
<text x="100" y="70" text-anchor="middle" class="node-detail">Production Observability</text>
</g>
<!-- Local DB -->
<g transform="translate(580, 420)" filter="url(#shadow-soft)">
<rect width="200" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2"/>
<text x="100" y="35" text-anchor="middle" class="node-title">Local Database</text>
<text x="100" y="55" text-anchor="middle" class="node-detail">Persistent Feedback Log</text>
<text x="100" y="70" text-anchor="middle" class="node-detail">SQLAlchemy / PostgreSQL</text>
</g>
<!-- Feedback Agent -->
<g transform="translate(380, 560)" filter="url(#shadow-soft)">
<rect width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5"/>
<text x="100" y="35" text-anchor="middle" class="node-title">Feedback Agent</text>
<text x="100" y="55" text-anchor="middle" class="node-detail">LLM Learning Loop</text>
<text x="100" y="70" text-anchor="middle" class="node-detail">Rules Optimization</text>
</g>
<!-- System Rules -->
<g transform="translate(380, 660)" filter="url(#shadow-soft)">
<rect width="200" height="40" rx="8" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2"/>
<text x="100" y="25" text-anchor="middle" class="node-title" style="font-size: 14px;">fashion_rules.json</text>
</g>
<!-- Arrows -->
<!-- User -> Pipeline -->
<path d="M 480 180 L 480 250" stroke="#5a5a5a" stroke-width="2.5" fill="none" marker-end="url(#arrow-claude)"/>
<rect x="425" y="205" width="110" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="480" y="220" text-anchor="middle" class="arrow-label">POST /api/feedback</text>
<!-- Pipeline -> Langfuse -->
<path d="M 380 300 L 280 300 L 280 410" stroke="#5a5a5a" stroke-width="2" fill="none" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
<rect x="230" y="345" width="100" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="280" y="360" text-anchor="middle" class="arrow-label">push_scores()</text>
<!-- Pipeline -> Local DB -->
<path d="M 580 300 L 680 300 L 680 410" stroke="#5a5a5a" stroke-width="2" fill="none" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
<rect x="630" y="345" width="100" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="680" y="360" text-anchor="middle" class="arrow-label">save_feedback()</text>
<!-- Langfuse -> Feedback Agent -->
<path d="M 280 500 L 280 600 L 370 600" stroke="#5a5a5a" stroke-width="2" fill="none" marker-end="url(#arrow-claude)"/>
<rect x="235" y="540" width="120" height="20" fill="#f8f6f3" opacity="0.9"/>
<text x="295" y="555" text-anchor="middle" class="arrow-label">fetch_bad_feedbacks()</text>
<!-- Feedback Agent -> Rules -->
<path d="M 480 640 L 480 655" stroke="#5a5a5a" stroke-width="2.5" fill="none" marker-end="url(#arrow-claude)"/>
<!-- Rules -> User (Loop) -->
<path d="M 580 680 L 880 680 L 880 140 L 580 140" stroke="#5a5a5a" stroke-width="2" fill="none" stroke-dasharray="8,4" marker-end="url(#arrow-claude)"/>
<text x="870" y="410" text-anchor="middle" class="arrow-label" transform="rotate(-90, 870, 410)">Continuous Improvement Loop</text>
<!-- Legend -->
<g transform="translate(680, 580)">
<rect width="240" height="80" rx="8" fill="#ffffff" stroke="#4a4a4a" stroke-width="1.5"/>
<text x="15" y="25" class="node-title" style="font-size: 13px;">Legend</text>
<line x1="15" y1="40" x2="45" y2="40" stroke="#5a5a5a" stroke-width="2"/>
<text x="55" y="45" class="node-detail">Primary Flow / Read</text>
<line x1="15" y1="60" x2="45" y2="60" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3"/>
<text x="55" y="65" class="node-detail">Async Write / Store</text>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700" width="960" height="700">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }
.title { font-size: 20px; font-weight: 700; fill: #1a1a1a; }
.node-label { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.node-desc { font-size: 13px; fill: #6a6a6a; }
.arrow-label { font-size: 12px; fill: #5a5a5a; }
.layer-label { font-size: 14px; font-weight: 600; fill: #6a6a6a; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow-soft">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>
</filter>
</defs>
<!-- Background -->
<rect width="960" height="700" fill="#f8f6f3"/>
<text x="480" y="40" text-anchor="middle" class="title">Product Performance &amp; Stock Sync Architecture</text>
<!-- Layers Labels -->
<text x="50" y="120" class="layer-label">Client</text>
<text x="50" y="240" class="layer-label">API</text>
<text x="50" y="380" class="layer-label">Logic</text>
<text x="50" y="520" class="layer-label">Data</text>
<!-- Nodes -->
<!-- Client UI -->
<rect x="380" y="80" width="200" height="80" rx="12" ry="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="115" text-anchor="middle" class="node-label">Web UI / Agent</text>
<text x="480" y="135" text-anchor="middle" class="node-desc">Stock Status Inquiry</text>
<!-- Product Route -->
<rect x="380" y="200" width="200" height="80" rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="235" text-anchor="middle" class="node-label">Product Route</text>
<text x="480" y="255" text-anchor="middle" class="node-desc">FastAPI Orchestrator</text>
<!-- StarRocks -->
<rect x="130" y="200" width="180" height="80" rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="220" y="235" text-anchor="middle" class="node-label">StarRocks</text>
<text x="220" y="255" text-anchor="middle" class="node-desc">SKU Expansion Engine</text>
<!-- Parallel Fetching -->
<rect x="380" y="340" width="200" height="80" rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="375" text-anchor="middle" class="node-label">Parallel Fetching</text>
<text x="480" y="395" text-anchor="middle" class="node-desc">asyncio.gather (Chunks)</text>
<!-- Stock Cache -->
<rect x="650" y="340" width="180" height="80" rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="740" y="375" text-anchor="middle" class="node-label">Stock Cache</text>
<text x="740" y="395" text-anchor="middle" class="node-desc">Redis / State Store</text>
<!-- Canifa ERP API -->
<rect x="380" y="480" width="200" height="80" rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>
<text x="480" y="515" text-anchor="middle" class="node-label">Canifa Stock API</text>
<text x="480" y="535" text-anchor="middle" class="node-desc">External ERP System</text>
<!-- Edges -->
<!-- UI -> Route -->
<line x1="480" y1="160" x2="480" y2="200" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="485" y="185" class="arrow-label">POST /api/stock/check</text>
<!-- Route -> StarRocks -->
<line x1="380" y1="230" x2="310" y2="230" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="325" y="225" text-anchor="middle" class="arrow-label">Expand SKU</text>
<!-- StarRocks -> Route -->
<line x1="310" y1="250" x2="380" y2="250" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="345" y="265" text-anchor="middle" class="arrow-label">Variants</text>
<!-- Route -> Parallel -->
<line x1="480" y1="280" x2="480" y2="340" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="485" y="315" class="arrow-label">Chunked SKUs</text>
<!-- Parallel -> ERP -->
<line x1="480" y1="420" x2="480" y2="480" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="485" y="455" class="arrow-label">Parallel HTTP Get</text>
<!-- ERP -> Parallel -->
<path d="M 500,480 Q 530,450 500,420" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="525" y="455" class="arrow-label">Responses</text>
<!-- Parallel -> Cache -->
<line x1="580" y1="380" x2="650" y2="380" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
<text x="615" y="375" text-anchor="middle" class="arrow-label">Store</text>
<!-- Cache -> UI -->
<path d="M 740,340 C 740,120 580,120 580,120" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<text x="680" y="115" class="arrow-label">Final Stock Data</text>
<!-- Legend -->
<rect x="700" y="600" width="220" height="80" rx="12" ry="12" fill="#ffffff" stroke="#4a4a4a" stroke-width="1.5"/>
<text x="715" y="625" class="node-label" style="font-size: 14px;">Legend</text>
<line x1="715" y1="645" x2="745" y2="645" stroke="#5a5a5a" stroke-width="2"/>
<text x="755" y="650" class="node-desc">Read / Sync operation</text>
<line x1="715" y1="665" x2="745" y2="665" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3"/>
<text x="755" y="670" class="node-desc">Write / Async store</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600">
<style>
text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; }
.title { font-size: 24px; font-weight: 700; fill: #1a1a1a; }
.node-title { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
.node-sub { font-size: 14px; font-weight: 400; fill: #1a1a1a; }
</style>
<defs>
<marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
</marker>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#00000010"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3"/>
<text x="480" y="50" text-anchor="middle" class="title">Text-to-SQL Pipeline (V1 &amp; V2)</text>
<!-- V1 Flow -->
<rect x="50" y="100" width="410" height="420" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="255" y="130" text-anchor="middle" class="node-title">V1: AI Data Analyst</text>
<rect x="150" y="160" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="250" y="195" text-anchor="middle" class="node-title">Codex Engine</text>
<text x="250" y="215" text-anchor="middle" class="node-sub">• Schema Injection</text>
<rect x="150" y="300" width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="250" y="335" text-anchor="middle" class="node-title">SQL Validator</text>
<text x="250" y="355" text-anchor="middle" class="node-sub">• Regex Guardrails</text>
<!-- V2 Flow -->
<rect x="500" y="100" width="410" height="420" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="705" y="130" text-anchor="middle" class="node-title">V2: Vector RAG</text>
<rect x="600" y="160" width="200" height="80" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="700" y="195" text-anchor="middle" class="node-title">Embedding</text>
<text x="700" y="215" text-anchor="middle" class="node-sub">• text-embedding-3</text>
<rect x="600" y="300" width="200" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow)"/>
<text x="700" y="335" text-anchor="middle" class="node-title">Vector Search</text>
<text x="700" y="355" text-anchor="middle" class="node-sub">• approx_cosine_sim</text>
<!-- Common DB -->
<rect x="380" y="460" width="200" height="80" rx="12" fill="#ffffff" stroke="#4a4a4a" stroke-width="3" filter="url(#shadow)"/>
<text x="480" y="505" text-anchor="middle" class="node-title">StarRocks / DB</text>
<line x1="250" y1="380" x2="480" y2="460" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
<line x1="700" y1="380" x2="480" y2="460" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<marker id="grayarrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#6b7280" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">Regression Testing Loop Architecture</text>
<rect x="60" y="80" width="840" height="460" rx="8" fill="none" stroke="#e2e8f0" stroke-dasharray="5,5" />
<text x="70" y="100" class="container-label">System Flow</text>
<!-- Nodes -->
<rect x="120" y="140" width="180" height="100" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="210" y="180" text-anchor="middle" class="node-label">Frontend UI</text>
<text x="210" y="200" text-anchor="middle" class="sub-label">regression-test.js</text>
<text x="210" y="215" text-anchor="middle" class="sub-label">(localStorage Cases)</text>
<rect x="420" y="140" width="180" height="100" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="510" y="180" text-anchor="middle" class="node-label">Backend API</text>
<text x="510" y="200" text-anchor="middle" class="sub-label">regression_test_route.py</text>
<rect x="720" y="140" width="180" height="100" rx="8" fill="#ffffff" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="810" y="180" text-anchor="middle" class="node-label">Target Agent</text>
<text x="810" y="200" text-anchor="middle" class="sub-label">Prod / Dev URL</text>
<!-- Arrows -->
<path d="M 300,175 L 410,175" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="355" y="165" text-anchor="middle" class="arrow-label">POST test_case</text>
<path d="M 600,175 L 710,175" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="655" y="165" text-anchor="middle" class="arrow-label">POST /chat</text>
<path d="M 720,205 L 610,205" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#grayarrowhead)" />
<text x="665" y="225" text-anchor="middle" class="sub-label">AI Response</text>
<path d="M 420,205 L 310,205" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#grayarrowhead)" />
<text x="365" y="225" text-anchor="middle" class="sub-label">Result + Latency</text>
<!-- Info Box -->
<rect x="140" y="320" width="680" height="180" rx="8" fill="#f1f5f9" stroke="#94a3b8" />
<text x="480" y="350" text-anchor="middle" class="node-label">Client-side Logic (Loop)</text>
<text x="160" y="385" class="sub-label">1. Read cases from localStorage</text>
<text x="160" y="410" class="sub-label">2. Loop: Send 1 request per iteration to keep UI responsive</text>
<text x="160" y="435" class="sub-label">3. Update Progress Bar &amp; Log real-time results</text>
<text x="160" y="460" class="sub-label">4. Final synthesis: Pass/Fail report + Average Latency</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "17-regression-test",
"title": "Regression Testing Suite",
"description": "Hướng dẫn chi tiết về kiến trúc bộ kiểm thử hồi quy (Regression Test), cách luồng dữ liệu tương tác giữa UI và Backend nhằm đảm bảo chất lượng phản hồi từ chatbot sau mỗi lần cập nhật prompt hoặc mô hình AI.",
"diagram": "diagrams/testing-suite.svg",
"diagram": "data/17-regression-test/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">Stress &amp; Load Testing Architecture</text>
<!-- Layers -->
<rect x="120" y="100" width="180" height="80" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="210" y="135" text-anchor="middle" class="node-label">Locust / User</text>
<text x="210" y="155" text-anchor="middle" class="sub-label">Simulated Traffic</text>
<rect x="390" y="100" width="180" height="80" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="135" text-anchor="middle" class="node-label">StressTestRouter</text>
<text x="480" y="155" text-anchor="middle" class="sub-label">API Handler</text>
<rect x="390" y="240" width="180" height="80" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="275" text-anchor="middle" class="node-label">Async Semaphore</text>
<text x="480" y="295" text-anchor="middle" class="sub-label">Concurrency Control</text>
<rect x="660" y="240" width="180" height="80" rx="8" fill="#ffffff" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="750" y="275" text-anchor="middle" class="node-label">Chatbot API</text>
<text x="750" y="295" text-anchor="middle" class="sub-label">Target Under Test</text>
<!-- Connections -->
<path d="M 300,140 L 380,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="340" y="130" text-anchor="middle" class="arrow-label">Request</text>
<path d="M 480,180 L 480,230" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="520" y="210" text-anchor="middle" class="arrow-label">Tasks</text>
<path d="M 570,280 L 650,280" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="610" y="270" text-anchor="middle" class="arrow-label">POST /chat</text>
<!-- Results Loop -->
<path d="M 750,320 L 750,450 L 480,450 L 480,320" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrowhead)" />
<text x="615" y="440" text-anchor="middle" class="sub-label">Latency Metrics (P50, P95, P99)</text>
<!-- Detailed Box -->
<rect x="120" y="480" width="720" height="100" rx="8" fill="#f1f5f9" stroke="#94a3b8" />
<text x="480" y="510" text-anchor="middle" class="node-label">Key Features</text>
<text x="140" y="540" class="sub-label">• locustfile.py: Supports ChatUser (Realistic) and QuickBurst (Stress) scenarios.</text>
<text x="140" y="565" class="sub-label">• Built-in API: Uses asyncio.Semaphore to limit concurrency without external tools.</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "18-stress-test",
"title": "Stress & Load Testing",
"description": "Hướng dẫn thực hiện kiểm thử tải để xác định giới hạn chịu tải của hệ thống và latency ở mức concurrency cao.",
"diagram": "diagrams/testing-suite.svg",
"diagram": "data/18-stress-test/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">User Persona Simulation Workflow</text>
<!-- Layers -->
<rect x="390" y="100" width="180" height="100" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="140" text-anchor="middle" class="node-label">User Simulator API</text>
<text x="480" y="160" text-anchor="middle" class="sub-label">Simulation Controller</text>
<rect x="120" y="100" width="180" height="100" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="210" y="140" text-anchor="middle" class="node-label">Persona Generator</text>
<text x="210" y="160" text-anchor="middle" class="sub-label">LLM Archetypes</text>
<rect x="660" y="100" width="180" height="100" rx="8" fill="#ffffff" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="750" y="140" text-anchor="middle" class="node-label">Chatbot Agent</text>
<text x="750" y="160" text-anchor="middle" class="sub-label">System Under Test</text>
<rect x="390" y="300" width="180" height="100" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="340" text-anchor="middle" class="node-label">Evaluation Engine</text>
<text x="480" y="360" text-anchor="middle" class="sub-label">Success/Drop Analysis</text>
<!-- Connections -->
<path d="M 300,150 L 380,150" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="340" y="140" text-anchor="middle" class="sub-label">Personas</text>
<path d="M 570,140 L 650,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="610" y="130" text-anchor="middle" class="arrow-label">User Msg</text>
<path d="M 650,160 L 580,160" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrowhead)" />
<text x="615" y="180" text-anchor="middle" class="sub-label">Bot Resp</text>
<path d="M 480,200 L 480,290" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="530" y="250" text-anchor="middle" class="arrow-label">History</text>
<!-- Info Box -->
<rect x="120" y="440" width="720" height="120" rx="8" fill="#f1f5f9" stroke="#94a3b8" />
<text x="480" y="470" text-anchor="middle" class="node-label">Simulation Logic</text>
<text x="140" y="500" class="sub-label">1. Generate Persona: Identity, Shopping Context, Chat Behavior (MBTI, Style).</text>
<text x="140" y="525" class="sub-label">2. Interaction: AI-to-AI dialogue to test Conversion vs. Drop triggers.</text>
<text x="140" y="550" class="sub-label">3. Evaluation: Cross-persona report on helpfulness and sales effectiveness.</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "19-user-simulator",
"title": "User Persona Simulation",
"description": "Sử dụng AI để sinh ra các 'Persona' khách hàng (MiroFish-inspired) nhằm kiểm thử tự động khả năng tư vấn và chốt sale của chatbot.",
"diagram": "diagrams/testing-suite.svg",
"diagram": "data/19-user-simulator/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">Community Reaction Engine Architecture</text>
<!-- Input -->
<rect x="60" y="100" width="160" height="80" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="140" y="135" text-anchor="middle" class="node-label">Marketing Content</text>
<text x="140" y="155" text-anchor="middle" class="sub-label">Campaign / News</text>
<!-- Agent -->
<rect x="300" y="100" width="180" height="80" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="135" text-anchor="middle" class="node-label">Reaction Agent</text>
<text x="390" y="155" text-anchor="middle" class="sub-label">Simulation Orchestrator</text>
<!-- Persona Segments -->
<g transform="translate(560, 80)">
<rect width="340" height="240" rx="8" fill="none" stroke="#e2e8f0" stroke-dasharray="5,5" />
<text x="10" y="20" class="container-label">Persona Segments</text>
<rect x="20" y="40" width="140" height="40" rx="4" fill="#ffffff" stroke="#94a3b8" />
<text x="90" y="65" text-anchor="middle" class="sub-label">Mom Shopper (20%)</text>
<rect x="180" y="40" width="140" height="40" rx="4" fill="#ffffff" stroke="#94a3b8" />
<text x="250" y="65" text-anchor="middle" class="sub-label">Young Prof (20%)</text>
<rect x="20" y="100" width="140" height="40" rx="4" fill="#ffffff" stroke="#94a3b8" />
<text x="90" y="125" text-anchor="middle" class="sub-label">Gen Z Trendy (15%)</text>
<rect x="180" y="100" width="140" height="40" rx="4" fill="#ffffff" stroke="#94a3b8" />
<text x="250" y="125" text-anchor="middle" class="sub-label">Fashionista (15%)</text>
<rect x="100" y="160" width="140" height="40" rx="4" fill="#ffffff" stroke="#94a3b8" />
<text x="170" y="185" text-anchor="middle" class="sub-label">Social Troll (5%)</text>
</g>
<!-- LLM -->
<rect x="300" y="240" width="180" height="80" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="275" text-anchor="middle" class="node-label">LLM Factory</text>
<text x="390" y="295" text-anchor="middle" class="sub-label">JSON Generation</text>
<!-- Output -->
<rect x="300" y="420" width="360" height="120" rx="8" fill="#f1f5f9" stroke="#94a3b8" />
<text x="480" y="450" text-anchor="middle" class="node-label">Synthesis &amp; Recommendations</text>
<text x="320" y="480" class="sub-label">• Sentiment Analysis (Pos / Neu / Neg)</text>
<text x="320" y="500" class="sub-label">• Action Advice (Launch / Delay / Response Template)</text>
<text x="320" y="520" class="sub-label">• Key Concerns extraction</text>
<!-- Connections -->
<path d="M 220,140 L 290,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<path d="M 390,180 L 390,230" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<path d="M 480,140 L 550,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<path d="M 480,280 L 550,280" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<path d="M 390,320 L 390,410" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "20-reaction-simulator",
"title": "Community Reaction Simulator",
"description": "Giả lập phản ứng của cộng đồng mạng (theo Persona) đối với các chiến dịch marketing bằng cách sử dụng LLM.",
"diagram": "diagrams/testing-suite.svg",
"diagram": "data/20-reaction-simulator/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">Hybrid Search FAQ Match Algorithm</text>
<!-- Flow -->
<rect x="100" y="100" width="160" height="80" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="180" y="135" text-anchor="middle" class="node-label">User Query</text>
<text x="180" y="155" text-anchor="middle" class="sub-label">Natural Language</text>
<rect x="340" y="100" width="160" height="80" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="420" y="135" text-anchor="middle" class="node-label">Normalizer</text>
<text x="420" y="155" text-anchor="middle" class="sub-label">Lower, No Accents</text>
<rect x="580" y="100" width="280" height="120" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-width="2" filter="url(#shadow)" />
<text x="720" y="135" text-anchor="middle" class="node-label">Similarity Scoring</text>
<text x="600" y="165" class="sub-label">• Word Overlap (70% weight)</text>
<text x="600" y="185" class="sub-label">• Fuzzy Ratio (30% weight)</text>
<text x="600" y="205" class="sub-label">• Threshold > 5.0 points</text>
<rect x="340" y="300" width="160" height="80" rx="8" fill="#ffffff" stroke="#94a3b8" stroke-dasharray="4,2" />
<text x="420" y="335" text-anchor="middle" class="node-label">FAQ Database</text>
<text x="420" y="355" text-anchor="middle" class="sub-label">SQLite (faqs table)</text>
<rect x="580" y="300" width="280" height="100" rx="8" fill="#f1f5f9" stroke="#2563eb" stroke-width="2" />
<text x="720" y="335" text-anchor="middle" class="node-label">Best Match Results</text>
<text x="720" y="355" text-anchor="middle" class="sub-label">Top 3 Sorted by Score</text>
<text x="720" y="375" text-anchor="middle" class="sub-label">Includes Variants</text>
<!-- Connections -->
<path d="M 260,140 L 330,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<path d="M 500,140 L 570,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<path d="M 420,290 L 420,190" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrowhead)" />
<text x="380" y="240" text-anchor="middle" class="sub-label" transform="rotate(-90, 380, 240)">Fetch all FAQs</text>
<path d="M 720,220 L 720,290" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "21-faq-manager",
"title": "FAQ Manager & BM25 Search",
"description": "Phân tích chuyên sâu (Deep Dive) về hệ thống FAQ Manager, cơ chế sinh biến thể AI (Variant Generation) và thuật toán Hybrid Search (kết hợp Word Overlap và Fuzzy Matching) dựa trên source code thực tế.",
"diagram": "diagrams/faq-bm25-search.svg",
"diagram": "data/21-faq-manager/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">Test-Driven Prompt Optimization (TDPE)</text>
<!-- Step 1 -->
<rect x="60" y="80" width="240" height="460" rx="8" fill="none" stroke="#2563eb" stroke-dasharray="5,5" />
<text x="70" y="100" class="container-label">Step 1: Run Test</text>
<rect x="90" y="120" width="180" height="60" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="180" y="155" text-anchor="middle" class="node-label">Chatbot Agent</text>
<text x="180" y="220" text-anchor="middle" class="sub-label">Extract: Response &amp; Product IDs</text>
<!-- Step 2 -->
<rect x="360" y="80" width="240" height="460" rx="8" fill="none" stroke="#059669" stroke-dasharray="5,5" />
<text x="370" y="100" class="container-label">Step 2: AI Judge</text>
<rect x="390" y="120" width="180" height="80" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="155" text-anchor="middle" class="node-label">AI Judge LLM</text>
<text x="480" y="175" text-anchor="middle" class="sub-label">Factual Verify</text>
<rect x="390" y="240" width="180" height="60" rx="8" fill="#ffffff" stroke="#94a3b8" stroke-dasharray="4,2" />
<text x="480" y="275" text-anchor="middle" class="node-label">Internal APIs</text>
<text x="480" y="320" text-anchor="middle" class="sub-label">Product Specs &amp; Stock</text>
<!-- Step 3 -->
<rect x="660" y="80" width="240" height="460" rx="8" fill="none" stroke="#ea580c" stroke-dasharray="5,5" />
<text x="670" y="100" class="container-label">Step 3: Optimizer</text>
<rect x="690" y="120" width="180" height="80" rx="8" fill="#ffffff" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="780" y="155" text-anchor="middle" class="node-label">AI Optimizer</text>
<text x="780" y="175" text-anchor="middle" class="sub-label">Prompt Rewriting</text>
<!-- Global Connections -->
<path d="M 270,150 L 380,150" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="325" y="140" text-anchor="middle" class="arrow-label">Output</text>
<path d="M 480,235 L 480,205" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrowhead)" />
<text x="530" y="225" text-anchor="middle" class="sub-label">Ground Truth</text>
<path d="M 570,150 L 680,150" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="625" y="140" text-anchor="middle" class="arrow-label">Metrics</text>
<!-- Info -->
<rect x="90" y="380" width="780" height="140" rx="8" fill="#f1f5f9" stroke="#94a3b8" />
<text x="480" y="410" text-anchor="middle" class="node-label">Optimization Loop</text>
<text x="110" y="440" class="sub-label">1. Judge assesses 5 criteria: factual accuracy, relevance, tone, helpfulness, and hallucinations.</text>
<text x="110" y="465" class="sub-label">2. Fails are analyzed to find root cause of incorrect product info or wrong tone.</text>
<text x="110" y="490" class="sub-label">3. AI Optimizer generates a NEW system prompt to fix issues while maintaining PASS status for good cases.</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "22-prompt-optimizer",
"title": "Prompt Optimizer",
"description": "Hệ thống Test-Driven Prompt Engineering (TDPE) sử dụng AI Judge để chấm điểm phản hồi và tự động tối ưu hóa System Prompt dựa trên các test case passing/failing.",
"diagram": "diagrams/prompt-management.svg",
"diagram": "data/22-prompt-optimizer/diagram.svg",
"sections": [
{
"id": "sec0",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<style>
.title { font-size: 20px; font-weight: bold; fill: #1a1a1a; font-family: sans-serif; }
.node-label { font-size: 14px; font-weight: 500; fill: #333; font-family: sans-serif; }
.sub-label { font-size: 11px; fill: #666; font-family: sans-serif; }
.arrow-label { font-size: 12px; fill: #2563eb; font-weight: 500; font-family: sans-serif; }
.container-label { font-size: 12px; font-weight: bold; fill: #94a3b8; font-family: sans-serif; text-transform: uppercase; }
</style>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1"/>
</filter>
</defs>
<rect width="960" height="600" fill="#f8f6f3" />
<text x="480" y="40" text-anchor="middle" class="title">Multi-Layer Protection Architecture</text>
<!-- Layers -->
<rect x="100" y="100" width="160" height="80" rx="8" fill="#ffffff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)" />
<text x="180" y="135" text-anchor="middle" class="node-label">Client</text>
<text x="180" y="155" text-anchor="middle" class="sub-label">Guest / User</text>
<rect x="340" y="100" width="180" height="100" rx="8" fill="#ffffff" stroke="#059669" stroke-width="2" filter="url(#shadow)" />
<text x="430" y="140" text-anchor="middle" class="node-label">RateLimitService</text>
<text x="430" y="160" text-anchor="middle" class="sub-label">SlowAPI (30/min)</text>
<text x="430" y="175" text-anchor="middle" class="sub-label">Memory Backend</text>
<rect x="600" y="100" width="200" height="100" rx="8" fill="#ffffff" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="700" y="140" text-anchor="middle" class="node-label">MessageLimitService</text>
<text x="700" y="160" text-anchor="middle" class="sub-label">Redis Quota (Daily)</text>
<text x="700" y="175" text-anchor="middle" class="sub-label">Guest: 10, User: 100</text>
<rect x="340" y="300" width="460" height="100" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-width="2" filter="url(#shadow)" />
<text x="570" y="340" text-anchor="middle" class="node-label">Agent Controller (LLM Execution)</text>
<text x="570" y="360" text-anchor="middle" class="sub-label">Calls Semantic Cache &amp; External LLM</text>
<!-- Connections -->
<path d="M 260,140 L 330,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="295" y="130" text-anchor="middle" class="arrow-label">Check</text>
<path d="M 520,140 L 590,140" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="555" y="130" text-anchor="middle" class="arrow-label">Pass</text>
<path d="M 700,200 L 700,290" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrowhead)" />
<text x="750" y="245" text-anchor="middle" class="arrow-label">Execute</text>
<path d="M 340,350 L 260,350 L 260,180" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrowhead)" />
<text x="180" y="245" text-anchor="middle" class="sub-label">Increment Quota &amp; Response</text>
<!-- Error paths -->
<path d="M 430,200 L 430,240 L 180,240 L 180,190" fill="none" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="2,2" marker-end="url(#arrowhead)" />
<text x="300" y="235" text-anchor="middle" class="sub-label" fill="#ef4444">429 Rate Exceeded</text>
</svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
"id": "24-cache-rate-limit",
"title": "Cache & Rate Limiting",
"description": "Chi tiết về cơ chế bảo vệ hệ thống khỏi spam và tối ưu chi phí LLM thông qua lớp Cache/Rate Limit.",
"diagram": "diagrams/auth-routing.svg",
"diagram": "data/24-cache-rate-limit/diagram.svg",
"sections": [
{
"id": "sec0",
......
import sys
import io
import json
import urllib.request
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
data = json.dumps({"user_query": "d? di an cu?i", "user_insight": None, "chat_history_summary": ""}).encode('utf-8')
req = urllib.request.Request("http://localhost:5000/api/agent/lead-stage", data=data, headers={'Content-Type': 'application/json'})
try:
with urllib.request.urlopen(req) as f:
print(f.read().decode('utf-8'))
except Exception as e:
print(e)
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE foo (a INT, b INT)')
# try to reproduce the exact error
try:
conn.execute('SELECT * FROM foo WHERE ("LOWER(A) NOT LIKE ?", "LOWER(A) NOT LIKE ?")')
except Exception as e:
print('TEST 1 ERROR:', repr(e))
try:
conn.execute('SELECT * FROM foo WHERE LOWER(A) NOT LIKE ? AND LOWER(A) NOT LIKE ?', ('%a%', '%b%'))
except Exception as e:
print('TEST 2 ERROR:', repr(e))
try:
conn.execute('SELECT * FROM foo WHERE "LOWER(A) NOT LIKE ?, LOWER(A) NOT LIKE ?"')
except Exception as e:
print('TEST 3 ERROR:', repr(e))
try:
conn.execute('SELECT * FROM foo WHERE "LOWER(A) NOT LIKE ?", "LOWER(A) NOT LIKE ?"')
except Exception as e:
print('TEST 4 ERROR:', repr(e))
try:
conn.execute('SELECT * FROM foo WHERE \'LOWER(A) NOT LIKE ?\', \'LOWER(A) NOT LIKE ?\'')
except Exception as e:
print('TEST 5 ERROR:', repr(e))
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE foo (a INT, b INT)')
try:
conn.execute('SELECT * FROM foo WHERE "LOWER(description_text) NOT LIKE ?, LOWER(description_text) NOT LIKE ?"')
except Exception as e:
print('TEST ERROR:', repr(e))
import sys
sys.stdout.reconfigure(encoding='utf-8')
import sqlite3
import json
import os
db_path = os.path.join("database", "canifa_ai_dump.sqlite")
print(f"Checking DB: {db_path}")
try:
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("PRAGMA table_info(pg__dashboard_canifa__ultra_descriptions);")
columns = cur.fetchall()
print("Columns:", [c[1] for c in columns])
cur.execute("SELECT base_ref_code, tags, description_data FROM pg__dashboard_canifa__ultra_descriptions WHERE tags IS NOT NULL AND tags != '' LIMIT 1;")
row = cur.fetchone()
if row:
product_code = row[0]
tags = row[1]
desc_data = row[2]
print("--- BEFORE UPDATE ---")
print("Product Code:", product_code)
print("Tags (Standalone):", tags)
print("Desc Data JSON:", desc_data)
# Parse it
try:
parsed_desc = json.loads(desc_data)
parsed_tags = json.loads(tags)
# Simulate update
parsed_desc["tags"] = parsed_tags
print("\n--- AFTER UPDATE (PREVIEW) ---")
print("New Desc Data JSON:", json.dumps(parsed_desc, ensure_ascii=False, indent=2))
except Exception as json_err:
print("JSON Parse Error:", json_err)
else:
print("No rows found with tags.")
cur.close()
conn.close()
except Exception as e:
print("DB Error:", e)
lines = []
lines.append('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700">')
lines.append(' <style>')
lines.append(' text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }')
lines.append(' </style>')
lines.append(' <defs>')
lines.append(' <marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">')
lines.append(' <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>')
lines.append(' </marker>')
lines.append(' <filter id="shadow-soft">')
lines.append(' <feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>')
lines.append(' </filter>')
lines.append(' </defs>')
lines.append(' <rect width="960" height="700" fill="#f8f6f3"/>')
lines.append(' <text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="22" font-weight="700">Canifa AI Platform Architecture</text>')
# Layer labels
lines.append(' <text x="40" y="120" fill="#6a6a6a" font-size="14" font-weight="600">Client</text>')
lines.append(' <text x="40" y="240" fill="#6a6a6a" font-size="14" font-weight="600">Interface</text>')
lines.append(' <text x="40" y="360" fill="#6a6a6a" font-size="14" font-weight="600">Gateway</text>')
lines.append(' <text x="40" y="480" fill="#6a6a6a" font-size="14" font-weight="600">Module</text>')
lines.append(' <text x="40" y="600" fill="#6a6a6a" font-size="14" font-weight="600">Data</text>')
# Nodes
# Client Layer
lines.append(' <rect x="380" y="80" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="115" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Web Browser</text>')
lines.append(' <text x="480" y="135" text-anchor="middle" fill="#6a6a6a" font-size="13">Static Entry Points</text>')
# Interface Layer
lines.append(' <rect x="380" y="200" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="235" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Micro-Frontends</text>')
lines.append(' <text x="480" y="255" text-anchor="middle" fill="#6a6a6a" font-size="13">Independent Namespaces</text>')
# Gateway Layer
lines.append(' <rect x="380" y="320" width="200" height="80" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="355" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">FastAPI Gateway</text>')
lines.append(' <text x="480" y="375" text-anchor="middle" fill="#6a6a6a" font-size="13">api_router &amp; Middleware</text>')
# Module Layer
lines.append(' <rect x="380" y="440" width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="475" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Business Modules</text>')
lines.append(' <text x="480" y="495" text-anchor="middle" fill="#6a6a6a" font-size="13">36+ Agents &amp; Event Bus</text>')
# Data Layer
lines.append(' <rect x="180" y="560" width="180" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="270" y="595" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Postgres DB</text>')
lines.append(' <text x="270" y="615" text-anchor="middle" fill="#6a6a6a" font-size="13">db_pool management</text>')
lines.append(' <rect x="390" y="560" width="180" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="595" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Redis Cache</text>')
lines.append(' <text x="480" y="615" text-anchor="middle" fill="#6a6a6a" font-size="13">redis_cache module</text>')
lines.append(' <rect x="600" y="560" width="180" height="80" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="690" y="595" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">OLAP (StarRocks)</text>')
lines.append(' <text x="690" y="615" text-anchor="middle" fill="#6a6a6a" font-size="13">Analytic Engine</text>')
# Arrows
lines.append(' <line x1="480" y1="160" x2="480" y2="200" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <line x1="480" y1="280" x2="480" y2="320" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <line x1="480" y1="400" x2="480" y2="440" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <path d="M 480 520 L 480 540 L 270 540 L 270 560" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <path d="M 480 520 L 480 560" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <path d="M 480 520 L 480 540 L 690 540 L 690 560" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append('</svg>')
output_path = "backend/static/cookbook/data/01-architecture/diagram.svg"
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"SVG generated at {output_path}")
lines = []
lines.append('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">')
lines.append(' <style>')
lines.append(' text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }')
lines.append(' </style>')
lines.append(' <defs>')
lines.append(' <marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">')
lines.append(' <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>')
lines.append(' </marker>')
lines.append(' <filter id="shadow-soft">')
lines.append(' <feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>')
lines.append(' </filter>')
lines.append(' </defs>')
lines.append(' <rect width="960" height="600" fill="#f8f6f3"/>')
lines.append(' <text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="22" font-weight="700">Database Connection &amp; Routing Logic</text>')
# Layer labels
lines.append(' <text x="40" y="100" fill="#6a6a6a" font-size="14" font-weight="600">Application</text>')
lines.append(' <text x="40" y="240" fill="#6a6a6a" font-size="14" font-weight="600">Router</text>')
lines.append(' <text x="40" y="420" fill="#6a6a6a" font-size="14" font-weight="600">Storage Layer</text>')
# App Layer
lines.append(' <rect x="380" y="70" width="200" height="80" rx="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="105" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">App / Helper</text>')
lines.append(' <text x="480" y="125" text-anchor="middle" fill="#6a6a6a" font-size="13">FastAPI or Class Helpers</text>')
# Logic/Router Layer
lines.append(' <rect x="380" y="200" width="200" height="80" rx="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="235" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Connection Switch</text>')
lines.append(' <text x="480" y="255" text-anchor="middle" fill="#6a6a6a" font-size="13">Check USE_LOCAL_SQLITE</text>')
# Local Mode
lines.append(' <rect x="180" y="380" width="200" height="100" rx="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="280" y="415" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">SQLite Mock</text>')
lines.append(' <text x="280" y="435" text-anchor="middle" fill="#6a6a6a" font-size="13">SQL Translation</text>')
lines.append(' <text x="280" y="455" text-anchor="middle" fill="#6a6a6a" font-size="13">sqlite_mock.py</text>')
# Production Mode
lines.append(' <rect x="580" y="380" width="200" height="100" rx="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5" filter="url(#shadow-soft)"/>')
lines.append(' <text x="680" y="415" text-anchor="middle" fill="#1a1a1a" font-size="16" font-weight="600">Postgres Pool</text>')
lines.append(' <text x="680" y="435" text-anchor="middle" fill="#6a6a6a" font-size="13">psycopg_pool / asyncpg</text>')
lines.append(' <text x="680" y="455" text-anchor="middle" fill="#6a6a6a" font-size="13">CanifaDbPool</text>')
# Arrows
lines.append(' <line x1="480" y1="150" x2="480" y2="200" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="490" y="180" fill="#5a5a5a" font-size="13">Connect/Query</text>')
lines.append(' <path d="M 380 240 L 280 240 L 280 380" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="200" y="310" fill="#5a5a5a" font-size="13">True (Local)</text>')
lines.append(' <path d="M 580 240 L 680 240 L 680 380" fill="none" stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="700" y="310" fill="#5a5a5a" font-size="13">False (Prod)</text>')
lines.append('</svg>')
output_path = "backend/static/cookbook/data/01b-database/diagram.svg"
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"SVG generated at {output_path}")
lines = []
lines.append('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">')
lines.append(' <style>')
lines.append(' text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; }')
lines.append(' </style>')
lines.append(' <defs>')
lines.append(' <marker id="arrow-claude" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">')
lines.append(' <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>')
lines.append(' </marker>')
lines.append(' <filter id="shadow-soft">')
lines.append(' <feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.05"/>')
lines.append(' </filter>')
lines.append(' </defs>')
lines.append(' <rect width="960" height="600" fill="#f8f6f3"/>')
lines.append(' <text x="480" y="40" text-anchor="middle" fill="#1a1a1a" font-size="22" font-weight="700">Authentication &amp; RBAC Flow</text>')
# Participants
# Browser
lines.append(' <rect x="100" y="100" width="200" height="400" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1" stroke-dasharray="5,5"/>')
lines.append(' <text x="200" y="125" text-anchor="middle" fill="#6a6a6a" font-size="14" font-weight="600">Browser / Frontend</text>')
# Backend
lines.append(' <rect x="380" y="100" width="200" height="400" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1" stroke-dasharray="5,5"/>')
lines.append(' <text x="480" y="125" text-anchor="middle" fill="#6a6a6a" font-size="14" font-weight="600">FastAPI Backend</text>')
# Database
lines.append(' <rect x="660" y="100" width="200" height="400" rx="12" fill="none" stroke="#4a4a4a" stroke-width="1" stroke-dasharray="5,5"/>')
lines.append(' <text x="760" y="125" text-anchor="middle" fill="#6a6a6a" font-size="14" font-weight="600">PostgreSQL DB</text>')
# Nodes
# Auth.js Guard
lines.append(' <rect x="120" y="160" width="160" height="60" rx="10" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>')
lines.append(' <text x="200" y="195" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">auth.js Guard</text>')
# API Login
lines.append(' <rect x="400" y="160" width="160" height="60" rx="10" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="195" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">POST /api/auth/login</text>')
# Admin Users Table
lines.append(' <rect x="680" y="160" width="160" height="60" rx="10" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>')
lines.append(' <text x="760" y="195" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">admin_users table</text>')
# Middleware
lines.append(' <rect x="400" y="300" width="160" height="60" rx="10" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>')
lines.append(' <text x="480" y="335" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">get_current_user</text>')
# LocalStorage
lines.append(' <rect x="120" y="300" width="160" height="60" rx="10" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2" filter="url(#shadow-soft)"/>')
lines.append(' <text x="200" y="335" text-anchor="middle" fill="#1a1a1a" font-size="14" font-weight="600">localStorage</text>')
# Arrows
lines.append(' <line x1="280" y1="180" x2="400" y2="180" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="340" y="175" text-anchor="middle" fill="#5a5a5a" font-size="11">Credentials</text>')
lines.append(' <line x1="560" y1="190" x2="680" y2="190" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="620" y="185" text-anchor="middle" fill="#5a5a5a" font-size="11">Verify</text>')
lines.append(' <line x1="400" y1="210" x2="280" y2="210" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="340" y="205" text-anchor="middle" fill="#5a5a5a" font-size="11">JWT Token</text>')
lines.append(' <line x1="200" y1="220" x2="200" y2="300" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="140" y="260" text-anchor="middle" fill="#5a5a5a" font-size="11">Store Token</text>')
lines.append(' <line x1="280" y1="330" x2="400" y2="330" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="340" y="325" text-anchor="middle" fill="#5a5a5a" font-size="11">Auth Header</text>')
lines.append(' <line x1="560" y1="330" x2="680" y2="190" stroke="#5a5a5a" stroke-width="1.5" marker-end="url(#arrow-claude)"/>')
lines.append(' <text x="630" y="270" text-anchor="middle" fill="#5a5a5a" font-size="11" transform="rotate(-30 630 270)">Check Role</text>')
lines.append('</svg>')
output_path = "backend/static/cookbook/data/01c-auth-routing/diagram.svg"
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"SVG generated at {output_path}")
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