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

chore: cleanup trash files and apply updates

parent df87d2f6
...@@ -26,7 +26,8 @@ ...@@ -26,7 +26,8 @@
"PowerShell(Invoke-RestMethod -Uri \"http://localhost:5000/api/product-desc/tags-batch-status\" -UseBasicParsing)", "PowerShell(Invoke-RestMethod -Uri \"http://localhost:5000/api/product-desc/tags-batch-status\" -UseBasicParsing)",
"PowerShell($job = Start-Job -ScriptBlock { & .venv\\\\Scripts\\\\python.exe scripts\\\\generate_tags_batch.py --limit 1928 } ; Wait-Job $job -Timeout 2700 ; Receive-Job $job -Keep)", "PowerShell($job = Start-Job -ScriptBlock { & .venv\\\\Scripts\\\\python.exe scripts\\\\generate_tags_batch.py --limit 1928 } ; Wait-Job $job -Timeout 2700 ; Receive-Job $job -Keep)",
"PowerShell(Get-Content \"d:\\\\cnf\\\\chatbot-canifa-feedback\\\\backend\\\\api\\\\main_router.py\" | Select-Object -Index \\(100..108\\) | Format-Hex)", "PowerShell(Get-Content \"d:\\\\cnf\\\\chatbot-canifa-feedback\\\\backend\\\\api\\\\main_router.py\" | Select-Object -Index \\(100..108\\) | Format-Hex)",
"PowerShell(git checkout \"d:\\\\cnf\\\\chatbot-canifa-feedback\\\\backend\\\\api\\\\main_router.py\")" "PowerShell(git checkout \"d:\\\\cnf\\\\chatbot-canifa-feedback\\\\backend\\\\api\\\\main_router.py\")",
"WebSearch"
] ]
} }
} }
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
"playwright": { "playwright": {
"command": "npx", "command": "npx",
"args": ["@modelcontextprotocol/server-playwright"] "args": ["@modelcontextprotocol/server-playwright"]
},
"dbeaver": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://mcp.dbeaver.com"]
} }
} }
} }
...@@ -19,7 +19,6 @@ import json ...@@ -19,7 +19,6 @@ import json
import logging import logging
import os as _os import os as _os
import re import re
import sqlite3
import time import time
import httpx import httpx
...@@ -126,10 +125,6 @@ async def _enrich_with_stock(products: list[dict]) -> tuple[list[dict], bool, fl ...@@ -126,10 +125,6 @@ async def _enrich_with_stock(products: list[dict]) -> tuple[list[dict], bool, fl
# ═══════════════════════════════════════════════ # ═══════════════════════════════════════════════
# SQLite local DB path
# ═══════════════════════════════════════════════
from common.constants import SQLITE_DB_PATH
TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding" TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding"
SELECT_COLUMNS = """ SELECT_COLUMNS = """
...@@ -831,10 +826,6 @@ async def _enrich_with_outfit( ...@@ -831,10 +826,6 @@ async def _enrich_with_outfit(
) -> list[dict]: ) -> list[dict]:
if not products: if not products:
return products return products
if not _os.path.exists(SQLITE_DB_PATH):
return products
# Phase 1 & 2: Resolve occasion from tags & expand to top 5 products # Phase 1 & 2: Resolve occasion from tags & expand to top 5 products
TAGS_TO_OCCASION = { TAGS_TO_OCCASION = {
"occ:di_lam": "di_lam", "occ:di_lam": "di_lam",
...@@ -862,25 +853,20 @@ async def _enrich_with_outfit( ...@@ -862,25 +853,20 @@ async def _enrich_with_outfit(
return products return products
try: try:
conn = sqlite3.connect(SQLITE_DB_PATH) placeholders = ",".join(["%s"] * len(anchor_base_codes))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
placeholders = ",".join(["?"] * len(anchor_base_codes))
# Đọc ai_matches trực tiếp từ ultra_descriptions # Đọc ai_matches trực tiếp từ ultra_descriptions qua PostgreSQL
desc_rows = cursor.execute( sql = f"""
f"""
SELECT base_ref_code, clean_description, description_data_cut, ai_matches SELECT base_ref_code, clean_description, description_data_cut, ai_matches
FROM pg__dashboard_canifa__ultra_descriptions FROM dashboard_canifa.ultra_descriptions
WHERE base_ref_code IN ({placeholders}) WHERE base_ref_code IN ({placeholders})
""", """
anchor_base_codes, desc_rows = await db.execute_query_async(sql, params=tuple(anchor_base_codes))
).fetchall() if not desc_rows:
desc_rows = []
conn.close()
except Exception as e: except Exception as e:
logger.error("❌ SQLite outfit read error: %s", e) logger.error("❌ Postgres outfit read error: %s", e)
return products return products
import json import json
......
import logging import logging
import sqlite3 from common.db_pool import db_pool
from pathlib import Path from psycopg.rows import dict_row
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DB_PATH = Path(__file__).resolve().parents[3] / "database" / "autotrain.db"
class LangfusePuller: class LangfusePuller:
""" """
Module phụ trách cào dữ liệu bad-feedback từ hệ thống Langfuse (hoặc Database Log nội bộ). Module phụ trách cào dữ liệu bad-feedback từ hệ thống Langfuse (hoặc Database Log nội bộ).
...@@ -16,18 +14,17 @@ class LangfusePuller: ...@@ -16,18 +14,17 @@ class LangfusePuller:
def fetch_bad_feedbacks(self, limit: int = 10) -> list[dict]: def fetch_bad_feedbacks(self, limit: int = 10) -> list[dict]:
""" """
Kéo log các ca tư vấn bị người dùng chê bai từ autotrain.db. Kéo log các ca tư vấn bị người dùng chê bai từ PostgreSQL.
""" """
logger.info(f"Đang kéo top {limit} bad feedbacks từ autotrain.db...") logger.info(f"Đang kéo top {limit} bad feedbacks từ PostgreSQL (feedbacks)...")
try: try:
with sqlite3.connect(DB_PATH) as conn: with db_pool.get_conn() as conn:
conn.row_factory = sqlite3.Row with conn.cursor(row_factory=dict_row) as cur:
cur = conn.cursor() cur.execute("SELECT * FROM feedbacks WHERE status = 'pending' LIMIT %s", (limit,))
cur.execute("SELECT * FROM feedbacks WHERE status = 'pending' LIMIT ?", (limit,))
rows = cur.fetchall() rows = cur.fetchall()
return [dict(row) for row in rows] return [dict(row) for row in rows]
except Exception as e: except Exception as e:
logger.error(f"Error fetching feedbacks: {e}") logger.error(f"Error fetching feedbacks from Postgres: {e}")
return [] return []
langfuse_puller = LangfusePuller() langfuse_puller = LangfusePuller()
...@@ -3,7 +3,7 @@ import logging ...@@ -3,7 +3,7 @@ import logging
from common.constants import TABLE_FASHION_RULES from common.constants import TABLE_FASHION_RULES
from common.llm_factory import create_llm from common.llm_factory import create_llm
from common.sqlite_db import sqlite_db from common.db_pool import db_pool
from config import DEFAULT_MODEL from config import DEFAULT_MODEL
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -45,12 +45,12 @@ async def persist_new_rule(rule_data: dict) -> bool: ...@@ -45,12 +45,12 @@ async def persist_new_rule(rule_data: dict) -> bool:
try: try:
query = f""" query = f"""
INSERT INTO {TABLE_FASHION_RULES} (rule_type, rule_content, created_by, status) INSERT INTO {TABLE_FASHION_RULES} (rule_type, rule_content, created_by, status)
VALUES (?, ?, ?, 'active') VALUES (%s, %s, %s, 'active')
""" """
rule_type = rule_data.get('rule_type', 'general') rule_type = rule_data.get('rule_type', 'general')
rule_content = f"{rule_data.get('new_rule', '')} (Lý do: {rule_data.get('reasoning', '')})" rule_content = f"{rule_data.get('new_rule', '')} (Lý do: {rule_data.get('reasoning', '')})"
await sqlite_db.execute(query, (rule_type, rule_content, "feedback_learning_loop")) await db_pool.execute_async(query, (rule_type, rule_content, "feedback_learning_loop"))
logger.info(f"Đã lưu Rule mới vào não bộ: {rule_content[:50]}...") logger.info(f"Đã lưu Rule mới vào não bộ: {rule_content[:50]}...")
return True return True
except Exception as e: except Exception as e:
......
...@@ -143,33 +143,29 @@ class ImageSearchGraph: ...@@ -143,33 +143,29 @@ class ImageSearchGraph:
extracted_features_text += f"Raw tags: {', '.join(feats.get('raw_labels', []))}\n" extracted_features_text += f"Raw tags: {', '.join(feats.get('raw_labels', []))}\n"
if ocr_text: if ocr_text:
import sqlite3 from common.starrocks_connection import get_db_connection
from common.constants import SQLITE_DB_PATH
try: try:
conn = sqlite3.connect(SQLITE_DB_PATH) db = await get_db_connection()
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
keywords = [k for k in ocr_text.split() if len(k) > 2] keywords = [k for k in ocr_text.split() if len(k) > 2]
if keywords: if keywords:
like_clauses = " OR ".join(["clean_description LIKE ? OR product_name LIKE ?"] * len(keywords)) like_clauses = " OR ".join(["clean_description LIKE %s OR product_name LIKE %s"] * len(keywords))
params = [] params = []
for k in keywords: for k in keywords:
params.extend([f"%{k}%", f"%{k}%"]) params.extend([f"%{k}%", f"%{k}%"])
rows = cursor.execute( sql = f"""
f"SELECT product_name, internal_ref_code, product_image_url " SELECT product_name, internal_ref_code, product_image_url
f"FROM pg__dashboard_canifa__ultra_descriptions " FROM dashboard_canifa.ultra_descriptions
f"WHERE {like_clauses} LIMIT 5", WHERE {like_clauses} LIMIT 5
params """
).fetchall() rows = await db.execute_query_async(sql, params=tuple(params))
if rows: if rows:
extracted_features_text += f"→ Đã tìm thấy {len(rows)} sản phẩm khớp chữ '{ocr_text}' trong SQLite:\n" extracted_features_text += f"→ Đã tìm thấy {len(rows)} sản phẩm khớp chữ '{ocr_text}':\n"
for r in rows: for r in rows:
extracted_features_text += f" - [{r['internal_ref_code']}] {r['product_name']}\n" extracted_features_text += f" - [{r['internal_ref_code']}] {r['product_name']}\n"
conn.close()
except Exception as e: except Exception as e:
logger.error(f"Lỗi truy vấn SQLite OCR: {e}") logger.error(f"Lỗi truy vấn Postgres OCR: {e}")
except Exception as e: except Exception as e:
logger.error(f"Lỗi đọc ảnh cho Local Vision Model: {e}") logger.error(f"Lỗi đọc ảnh cho Local Vision Model: {e}")
......
"""Lead Stage Agent — AI #1 phân tích giai đoạn mua hàng của khách."""
"""
Lead Stage Agent Controller — Entry point with Langfuse tracing.
Wraps LeadStageGraph (Classifier ⇄ Tools → Stylist) with:
- Langfuse trace + span context
- Conversation history loading
- User insight injection
- Background task for history persistence
Every call creates a named Langfuse trace "lead-stage-chat" so the full
Classifier → Tool → Stylist pipeline is visible in the Langfuse dashboard.
"""
import logging
import time
import uuid
from fastapi import BackgroundTasks
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.runnables import RunnableConfig
from langfuse import Langfuse, get_client as get_langfuse
from agent.controller_helpers import load_user_insight_from_redis, save_user_insight_to_redis
from agent.helper import handle_post_chat_async
from common.conversation_manager import get_conversation_manager
from common.langfuse_client import get_callback_handler
from .graph import get_lead_stage_agent
logger = logging.getLogger(__name__)
async def lead_stage_chat_controller(
*,
query: str,
identity_key: str,
background_tasks: BackgroundTasks,
model_name: str | None = None,
conversation_id: str | None = None,
is_authenticated: bool = False,
device_id: str | None = None,
) -> dict:
"""
Controller cho Lead Stage Agent — full Langfuse tracing.
Flow:
1. Load user_insight từ Redis
2. Load chat history từ ConversationManager
3. Build Langfuse trace context
4. Run LeadStageGraph (Classifier ⇄ Tools → Stylist)
5. Save conversation background
6. Return response + pipeline + lead_stage metadata
Returns:
dict with: status, ai_response, products, lead_stage, pipeline, timing, trace_id
"""
start_time = time.time()
run_id = str(uuid.uuid4())
session_id = conversation_id or f"{identity_key}-lead-{run_id[:8]}"
logger.info(f"📥 [Lead Controller] identity={identity_key} | query={query[:80]}")
# ═══ 1. LOAD USER INSIGHT ═══
user_insight = None
if identity_key:
user_insight = await load_user_insight_from_redis(str(identity_key))
# ═══ 2. LOAD CHAT HISTORY ═══
memory = await get_conversation_manager()
history_dicts = await memory.get_chat_history(
str(identity_key),
limit=10,
include_product_ids=False,
conversation_id=conversation_id,
)
history_msgs = [
HumanMessage(content=m["message"]) if m["is_human"] else AIMessage(content=m["message"])
for m in history_dicts
][::-1]
# ═══ 3. BUILD LANGFUSE TRACE ═══
trace_id = Langfuse.create_trace_id()
tags = ["lead_stage", "experiment"]
if is_authenticated:
tags.append("user:authenticated")
else:
tags.append("user:anonymous")
langfuse = get_langfuse()
observation_ctx = None
if langfuse:
try:
observation_ctx = langfuse.start_as_current_observation(
as_type="span",
name="lead-stage-chat",
trace_context={"trace_id": trace_id},
)
except Exception as e:
logger.warning(f"⚠️ Langfuse span init failed: {e}")
# Enter observation context
span = None
if observation_ctx:
try:
span = observation_ctx.__enter__()
span.update_trace(
name="lead-stage-chat",
user_id=str(identity_key),
session_id=session_id,
tags=tags,
input={"query": query, "identity_key": identity_key},
metadata={
"device_id": device_id,
"customer_id": identity_key if is_authenticated else None,
"model": model_name,
"conversation_id": conversation_id,
"history_turns": len(history_msgs),
"has_user_insight": user_insight is not None,
},
)
except Exception as e:
logger.warning(f"⚠️ Langfuse trace update failed: {e}")
# Create CallbackHandler INSIDE observation context for proper nesting
langfuse_handler = get_callback_handler()
exec_config = RunnableConfig(
callbacks=[langfuse_handler] if langfuse_handler else [],
run_name="lead-stage-graph",
run_id=run_id,
metadata={
"langfuse_session_id": session_id,
"langfuse_user_id": str(identity_key),
"langfuse_tags": tags,
"trace_id": trace_id,
"conversation_id": session_id,
"customer_id": identity_key if is_authenticated else None,
"device_id": device_id,
},
)
# ═══ 4. RUN LEAD STAGE GRAPH ═══
try:
agent = get_lead_stage_agent(model_name)
chat_result = await agent.chat(
user_message=query,
user_insight=user_insight,
history=history_msgs,
config=exec_config,
)
ai_response = chat_result.get("response", "")
lead_stage = chat_result.get("lead_stage") or {}
pipeline = chat_result.get("pipeline", [])
products = chat_result.get("products", [])
total_elapsed = chat_result.get("elapsed_ms", 0)
# Timing breakdown from pipeline
classifier_ms = 0
stylist_ms = 0
tool_ms = 0
for p in pipeline:
step = p.get("step", "")
ms = p.get("elapsed_ms", 0)
if "classifier" in step:
classifier_ms += ms
elif step in ("stylist", "responder"):
stylist_ms += ms
elif step == "tool_result":
tool_ms += ms
# Update Langfuse trace output
if span:
try:
span.update_trace(
output={
"ai_response": (ai_response or "")[:500],
"stage": lead_stage.get("stage_name", "UNKNOWN"),
"stage_confidence": lead_stage.get("confidence"),
"product_count": len(products),
"elapsed_ms": round(total_elapsed),
"classifier_ms": classifier_ms,
"stylist_ms": stylist_ms,
},
)
except Exception:
pass
logger.info(
f"✅ [Lead Controller] Stage: {lead_stage.get('stage_name', 'N/A')} | "
f"Products: {len(products)} | "
f"Time: {total_elapsed:.0f}ms (C:{classifier_ms}ms S:{stylist_ms}ms)"
)
# ═══ 5. SAVE CONVERSATION (background) ═══
# Chỉ lưu text của Stylist + compact product list (sku + name)
compact_products = [
{"sku": p.get("sku_code") or p.get("sku") or p.get("internal_ref_code", ""),
"name": p.get("name") or p.get("product_name", "")}
for p in products[:6]
]
response_payload = {
"ai_response": ai_response, # Chỉ text của Stylist
"product_ids": compact_products, # Compact: SKU + tên (không full object)
"lead_stage": lead_stage,
}
background_tasks.add_task(
handle_post_chat_async,
memory=memory,
identity_key=str(identity_key),
human_query=query,
ai_response=response_payload,
conversation_id=conversation_id,
)
# ── Persist Updated Insight về Redis ──
updated_insight = chat_result.get("updated_insight")
if updated_insight and identity_key:
import json as _json
insight_str = _json.dumps(updated_insight, ensure_ascii=False)
background_tasks.add_task(save_user_insight_to_redis, str(identity_key), insight_str)
logger.info(f"💾 [Lead Controller] Queued insight persist | stage={updated_insight.get('STAGE', '?')}")
# Lưu Postgres (persistent, không bị TTL như Redis)
from common.lead_flow_postgres import save_lead_turn
background_tasks.add_task(
save_lead_turn,
device_id=str(device_id or identity_key),
conv_id=str(conversation_id or session_id),
human_message=query,
ai_response_text=ai_response,
products=products,
lead_stage=lead_stage,
)
# ═══ 6. RETURN ═══
return {
"status": "success",
"ai_response": ai_response,
"products": products,
"trace_id": trace_id,
"lead_stage": lead_stage,
"pipeline": pipeline,
"timing": {
"classifier_ms": classifier_ms,
"stylist_ms": stylist_ms,
"tool_ms": tool_ms,
"total_ms": round(total_elapsed),
},
}
except Exception as e:
elapsed = round((time.time() - start_time) * 1000)
logger.error(f"❌ [Lead Controller] Error after {elapsed}ms: {e}", exc_info=True)
# Log error to Langfuse
if span:
try:
span.update_trace(
output={"error": str(e)[:500], "elapsed_ms": elapsed},
level="ERROR",
)
except Exception:
pass
return {
"status": "error",
"error_code": "LEAD_STAGE_ERROR",
"message": f"Lead Stage Error: {str(e)[:200]}",
"trace_id": trace_id,
}
finally:
# Close Langfuse observation context
if observation_ctx:
try:
observation_ctx.__exit__(None, None, None)
except Exception:
pass
This diff is collapsed.
This diff is collapsed.
"""
Lead Search Tool - LangChain @tool cho AI goi.
Lớp này giờ chỉ đóng vai trò Wrapper mỏng (Thin Wrapper).
Toàn bộ logic phức tạp đã được đưa sang ProductSearchEngine.
"""
import json
import logging
from langchain_core.tools import tool
from .product_search_engine import InferredSearch, LeadSearchInput, LiteralSearch, ProductSearchEngine
logger = logging.getLogger(__name__)
engine = ProductSearchEngine()
@tool(args_schema=LeadSearchInput)
async def lead_search_tool(
literal: LiteralSearch,
inferred: InferredSearch,
magento_ref_code: str | None = None,
reasoning: str | None = None,
user_insight: dict | None = None, # THÊM: từ graph state
) -> str:
"""
Tim kiem san pham CANIFA theo Dual-Lane Architecture.
"""
req = LeadSearchInput(
literal=literal,
inferred=inferred,
magento_ref_code=magento_ref_code,
reasoning=reasoning,
)
try:
# Gọi engine để xử lý logic dual-query / cascade
result_dict = await engine.search(req, reasoning=reasoning, user_insight=user_insight)
return json.dumps(result_dict, ensure_ascii=False, default=str)
except Exception as e:
logger.error("Lead search tool error: %s", e, exc_info=True)
return json.dumps({"status": "error", "message": str(e)})
"""
Product Line Mapping
Key = DB product_line_vn (chính xác)
Value = list các từ khách hàng hay dùng (synonym)
"""
# DB value → [các từ khách hàng hay gọi]
PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo Sơ mi": ["áo sơ mi", "áo công sở", "áo đi làm", "sơ mi", "sơmi", "áo sơmi"],
"Áo Polo": ["áo polo", "áo cổ bẻ", "polo"],
"Áo phông": ["áo phông", "áo thun", "áo thun ngắn tay", "áo cổ v", "áo cổ tym", "áo cộc tay"],
"Áo nỉ có mũ": ["áo nỉ có mũ", "áo hoodie", "hoodie"],
"Áo nỉ": ["áo nỉ", "áo sweater", "sweater"],
"Áo mặc nhà": ["áo mặc nhà", "áo ngủ", "áo ở nhà"],
"Áo lót": ["áo lót", "áo ngực", "áo quây", "áo lót nữ", "áo lót nam", "áo lót trẻ em"],
"Áo len gilet": ["áo len gilet", "áo gile len"],
"Áo len": ["áo len", "áo len dài tay"],
"Áo kiểu": ["áo kiểu", "áo điệu", "áo nữ tính"],
"Áo khoác sợi": ["áo khoác sợi"],
"Áo khoác nỉ không mũ": ["áo khoác nỉ không mũ", "áo khoác sweater"],
"Áo khoác nỉ có mũ": ["áo khoác nỉ có mũ", "áo khoác hoodie", "áo khoác nỉ"],
"Áo khoác lông vũ": ["áo khoác lông vũ", "áo phao lông vũ", "áo lông vũ"],
"Áo khoác gió": ["áo khoác gió", "áo gió", "áo khoác mỏng"],
"Áo khoác gilet chần bông": ["áo khoác gilet chần bông", "áo khoác gilet trần bông", "áo gilet chần bông", "áo gilet trần bông"],
"Áo khoác gilet": ["áo khoác gilet", "áo gile", "gile"],
"Áo khoác dạ": ["áo khoác dạ", "áo dạ"],
"Áo khoác dáng ngắn": ["áo khoác dáng ngắn", "áo khoác croptop"],
"Áo khoác chống nắng": ["áo khoác chống nắng", "áo chống nắng"],
"Áo khoác chần bông": ["áo khoác chần bông", "áo khoác trần bông", "áo chần bông", "áo trần bông", "áo phao"],
"Áo khoác": ["áo khoác", "áo ấm", "áo rét"],
"Áo giữ nhiệt": ["áo giữ nhiệt", "áo tản nhiệt", "áo heattech"],
"Áo bra active": ["áo bra active", "áo bra", "bra", "áo tập", "áo thể thao"],
"Áo Body": ["áo body", "áo croptop", "croptop", "baby tee", "áo lửng", "áo dáng ngắn", "áo ôm"],
"Áo ba lỗ": ["áo ba lỗ", "áo sát nách", "tanktop", "tank top", "áo dây", "áo 2 dây", "áo hai dây"],
"Váy liền": ["váy liền", "đầm", "váy công sở", "đầm công sở", "váy liền thân", "đầm suông", "váy dài", "váy body"],
"Chân váy": ["chân váy", "váy maxi", "váy midi", "chân váy dài", "chân váy chữ a", "chân váy công sở", "váy ngắn"],
"Quần giả váy": ["quần giả váy", "quần váy", "skort"],
"Quần soóc": ["quần soóc", "quần đùi", "quần short", "quần lửng", "quần ngố", "short", "quần đùi nam", "quần đùi nữ"],
"Quần nỉ": ["quần nỉ", "quần jogger", "quần ống bo chun", "jogger", "quần thể thao"],
"Quần mặc nhà": ["quần mặc nhà", "quần ngủ", "quần đùi mặc nhà"],
"Quần lót đùi": ["quần lót đùi", "quần sịp đùi", "quần boxer", "boxer", "sịp đùi", "quần xì đùi"],
"Quần lót tam giác": ["quần lót tam giác", "quần sịp tam giác", "quần brief", "brief", "sịp tam giác", "quần xì tam giác"],
"Quần lót": ["quần lót", "quần chip", "quần sịp", "quần trong", "quần nhỏ", "quần xơ lít", "quần xì", "sịp", "chip", "đồ lót"],
"Quần leggings mặc nhà": ["quần leggings mặc nhà", "quần legging mặc nhà"],
"Quần leggings": ["quần leggings", "leggings", "quần legging", "legging", "quần thun ôm"],
"Quần Khaki": ["quần khaki", "quần âu", "quần vải", "quần tây", "quần công sở", "quần đi làm", "quần âu nam", "quần âu nữ", "quần kaki"],
"Quần jean": ["quần jean", "quần bò", "quần jeans", "denim", "jeans", "bò", "jean", "quần dzin"],
"Quần giữ nhiệt": ["quần giữ nhiệt", "quần heattech"],
"Quần dài": ["quần dài", "quần suông", "quần ống rộng", "quần ống suông", "quần lưng thun"],
"Quần culottes": ["quần culottes", "culottes", "quần lửng ống rộng"],
"Quần Body": ["quần body", "quần ôm"],
"Pyjama": ["pyjama", "pajama", "đồ pijama"],
"Mũ": ["mũ", "nón", "phụ kiện Canifa", "phụ kiện"],
"Khăn tắm": ["khăn tắm", "khăn to", "phụ kiện"],
"Khăn mặt": ["khăn mặt", "khăn nhỏ", "phụ kiện"],
"Khăn lau đầu": ["khăn lau đầu", "phụ kiện"],
"Khăn": ["khăn", "khăn len", "khăn quàng cổ", "phụ kiện"],
"Găng tay chống nắng": ["găng tay chống nắng", "găng tay", "bao tay"],
"Chăn cá nhân": ["chăn cá nhân", "chăn", "mền"],
"Cardigan": ["cardigan", "áo khoác len", "áo cardigan"],
"Bộ thể thao": ["bộ thể thao", "đồ tập", "đồ thể thao"],
"Bộ quần áo": ["bộ quần áo", "đồ bộ", "set đồ"],
"Bộ mặc nhà": ["bộ mặc nhà", "đồ ngủ", "đồ mặc nhà", "đồ ở nhà", "bộ lanh"],
"Blazer": ["blazer", "áo vest", "vest"],
"Tất": ["tất", "vớ", "bao chân", "vớ chân", "tất chân"],
"Quần tất": ["quần tất", "quần vớ", "tất quần"],
"Mũ thể thao": ["mũ thể thao", "mũ snapback", "mũ lưỡi trai", "cap"],
"Khẩu trang": ["khẩu trang", "mask", "mặt nạ vải"],
"Túi xách": ["túi xách", "túi"],
}
# ==============================================================================
# AUTO-GENERATE reverse lookup: synonym → DB value
# "áo thun" → "Áo phông", "quần bò" → "Quần jean", ...
# ==============================================================================
SYNONYM_TO_DB: dict[str, str] = {}
for db_value, synonyms in PRODUCT_LINE_MAP.items():
for syn in synonyms:
SYNONYM_TO_DB[syn.lower()] = db_value
# ==============================================================================
# RELATED LINES: hỏi "áo bra" → tìm cả "Áo bra active" + "Áo lót" và ngược lại
# ==============================================================================
RELATED_LINES: dict[str, list[str]] = {
"Áo bra active": ["Áo lót"],
"Áo lót": ["Áo bra active"],
# Quần lót (chung) → mở rộng tìm cả Quần lót đùi (Trunk) + Quần lót tam giác (Brief)
"Quần lót": ["Quần lót đùi", "Quần lót tam giác"],
"Quần lót đùi": ["Quần lót", "Quần lót tam giác"],
"Quần lót tam giác": ["Quần lót", "Quần lót đùi"],
}
def get_related_lines(product_line: str) -> list[str]:
"""VD: get_related_lines("Áo bra active") → ["Áo bra active", "Áo lót"]"""
return [product_line] + RELATED_LINES.get(product_line, [])
# Pre-sort synonyms by length DESC for longest-match-first
_SORTED_SYNONYMS = sorted(SYNONYM_TO_DB.keys(), key=len, reverse=True)
def resolve_product_name(raw_name: str) -> str:
"""
Resolve synonym trong product_name → tên DB thật.
Dùng longest-match-first để tránh match sai. Cập nhật thay thế ở bất kỳ vị trí nào trong chuỗi.
VD:
"tìm áo cổ bẻ khaki" → "tìm áo polo khaki"
"áo thun disney" → "áo phông disney"
"quần bò ống rộng" → "quần jean ống rộng"
"""
result = raw_name.lower().strip()
# Custom rule cho sơ mi cộc tay -> sơ mi ngắn tay
if "sơ mi" in result and "cộc tay" in result:
result = result.replace("cộc tay", "ngắn tay")
for synonym in _SORTED_SYNONYMS:
if synonym in result:
db_value = SYNONYM_TO_DB[synonym]
# Thay thế tất cả các lần xuất hiện của synonym bằng db_value (để thường)
result = result.replace(synonym, db_value.lower())
return result
def resolve_product_line(raw_value: str) -> list[str]:
"""
Lookup keyword → DB product_line_vn.
Hỗ trợ '/' separator (VD: "Quần/ Váy").
Không tìm thấy → giữ nguyên (prefix match ở SQL).
"""
parts = [p.strip() for p in raw_value.split("/") if p.strip()]
resolved = []
for part in parts:
mapped = SYNONYM_TO_DB.get(part.lower())
if mapped:
resolved.append(mapped)
else:
resolved.append(part)
return resolved
This diff is collapsed.
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"
if 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"
if 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
...@@ -19,7 +19,6 @@ import json ...@@ -19,7 +19,6 @@ import json
import logging import logging
import os as _os import os as _os
import re import re
import sqlite3
import time import time
import httpx import httpx
...@@ -126,9 +125,8 @@ async def _enrich_with_stock(products: list[dict]) -> tuple[list[dict], bool, fl ...@@ -126,9 +125,8 @@ async def _enrich_with_stock(products: list[dict]) -> tuple[list[dict], bool, fl
# ═══════════════════════════════════════════════ # ═══════════════════════════════════════════════
# SQLite local DB path # PostgreSQL Table definition
# ═══════════════════════════════════════════════ # ═══════════════════════════════════════════════
from common.constants import SQLITE_DB_PATH
TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding" TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding"
...@@ -831,9 +829,6 @@ async def _enrich_with_outfit( ...@@ -831,9 +829,6 @@ async def _enrich_with_outfit(
if not products: if not products:
return products return products
if not _os.path.exists(SQLITE_DB_PATH):
return products
# Phase 1 & 2: Resolve occasion from tags & expand to top 5 products # Phase 1 & 2: Resolve occasion from tags & expand to top 5 products
TAGS_TO_OCCASION = { TAGS_TO_OCCASION = {
"occ:di_lam": "di_lam", "occ:di_lam": "di_lam",
...@@ -861,25 +856,20 @@ async def _enrich_with_outfit( ...@@ -861,25 +856,20 @@ async def _enrich_with_outfit(
return products return products
try: try:
conn = sqlite3.connect(SQLITE_DB_PATH) placeholders = ",".join(["%s"] * len(anchor_base_codes))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
placeholders = ",".join(["?"] * len(anchor_base_codes))
# Đọc ai_matches trực tiếp từ ultra_descriptions # Đọc ai_matches trực tiếp từ ultra_descriptions qua PostgreSQL
desc_rows = cursor.execute( sql = f"""
f"""
SELECT base_ref_code, clean_description, description_data_cut, ai_matches SELECT base_ref_code, clean_description, description_data_cut, ai_matches
FROM pg__dashboard_canifa__ultra_descriptions FROM dashboard_canifa.ultra_descriptions
WHERE base_ref_code IN ({placeholders}) WHERE base_ref_code IN ({placeholders})
""", """
anchor_base_codes, desc_rows = await db.execute_query_async(sql, params=tuple(anchor_base_codes))
).fetchall() if not desc_rows:
desc_rows = []
conn.close()
except Exception as e: except Exception as e:
logger.error("❌ SQLite outfit read error: %s", e) logger.error("❌ Postgres outfit read error: %s", e)
return products return products
import json import json
......
This diff is collapsed.
...@@ -19,7 +19,7 @@ def ensure_sql_tables() -> None: ...@@ -19,7 +19,7 @@ def ensure_sql_tables() -> None:
conn = get_pooled_connection_compat() conn = get_pooled_connection_compat()
cur = conn.cursor() cur = conn.cursor()
cur.execute(""" cur.execute("""
CREATE TABLE IF NOT EXISTS dashboard_canifa.sql_trace_sessions ( CREATE TABLE IF NOT EXISTS sql_trace_sessions (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
conversation_id UUID NOT NULL, conversation_id UUID NOT NULL,
session_id INT NOT NULL, session_id INT NOT NULL,
...@@ -33,7 +33,7 @@ def ensure_sql_tables() -> None: ...@@ -33,7 +33,7 @@ def ensure_sql_tables() -> None:
""") """)
cur.execute(""" cur.execute("""
CREATE INDEX IF NOT EXISTS idx_sql_trace_conv_id CREATE INDEX IF NOT EXISTS idx_sql_trace_conv_id
ON dashboard_canifa.sql_trace_sessions(conversation_id); ON sql_trace_sessions(conversation_id);
""") """)
cur.close() cur.close()
except Exception as e: except Exception as e:
...@@ -52,7 +52,7 @@ def persist_session_to_db(session: dict[str, Any]) -> None: ...@@ -52,7 +52,7 @@ def persist_session_to_db(session: dict[str, Any]) -> None:
public = public_session(session) public = public_session(session)
cur.execute( cur.execute(
""" """
INSERT INTO dashboard_canifa.sql_trace_sessions INSERT INTO sql_trace_sessions
(conversation_id, session_id, question, model, status, session_data, created_at, completed_at) (conversation_id, session_id, question, model, status, session_data, created_at, completed_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
...@@ -95,7 +95,7 @@ def load_conversations(limit: int = 50) -> list[dict[str, Any]]: ...@@ -95,7 +95,7 @@ def load_conversations(limit: int = 50) -> list[dict[str, Any]]:
MIN(created_at) AS created_at, MIN(created_at) AS created_at,
MAX(completed_at) AS updated_at, MAX(completed_at) AS updated_at,
COUNT(*) AS session_count COUNT(*) AS session_count
FROM dashboard_canifa.sql_trace_sessions FROM sql_trace_sessions
GROUP BY conversation_id GROUP BY conversation_id
ORDER BY MAX(created_at) DESC ORDER BY MAX(created_at) DESC
LIMIT %s LIMIT %s
...@@ -134,7 +134,7 @@ def load_conversation_detail(conv_id: str) -> list[dict[str, Any]]: ...@@ -134,7 +134,7 @@ def load_conversation_detail(conv_id: str) -> list[dict[str, Any]]:
cur.execute( cur.execute(
""" """
SELECT session_id, question, model, status, session_data, created_at, completed_at SELECT session_id, question, model, status, session_data, created_at, completed_at
FROM dashboard_canifa.sql_trace_sessions FROM sql_trace_sessions
WHERE conversation_id = %s WHERE conversation_id = %s
ORDER BY created_at ASC ORDER BY created_at ASC
""", """,
......
...@@ -59,12 +59,13 @@ async def run_pipeline(): ...@@ -59,12 +59,13 @@ async def run_pipeline():
conn = get_db() conn = get_db()
cur = conn.cursor() cur = conn.cursor()
cur.execute("INSERT INTO pipeline_runs (run_id, started_at) VALUES (?, ?)", (run_id, datetime.datetime.now().isoformat())) cur.execute("INSERT INTO pipeline_runs (run_id, started_at) VALUES (%s, %s)", (run_id, datetime.datetime.now().isoformat()))
conn.commit() conn.commit()
try: try:
cur.execute("SELECT * FROM feedbacks WHERE status = 'pending' LIMIT 10") cur.execute("SELECT * FROM feedbacks WHERE status = 'pending' LIMIT 10")
feedbacks = [dict(row) for row in cur.fetchall()] columns = [desc[0] for desc in cur.description] if cur.description else []
feedbacks = [dict(zip(columns, row)) for row in cur.fetchall()]
rules_generated = 0 rules_generated = 0
rules_approved = 0 rules_approved = 0
...@@ -79,21 +80,21 @@ async def run_pipeline(): ...@@ -79,21 +80,21 @@ async def run_pipeline():
if new_rule: if new_rule:
append_rule_to_prompt("02_rules.txt", new_rule) append_rule_to_prompt("02_rules.txt", new_rule)
cur.execute("UPDATE feedbacks SET status = 'processed' WHERE id = ?", (fb["id"],)) cur.execute("UPDATE feedbacks SET status = 'processed' WHERE id = %s", (fb["id"],))
except Exception as e: except Exception as e:
logger.error(f"Rule gen error for FB {fb.get('id')}: {e}") logger.error(f"Rule gen error for FB {fb.get('id')}: {e}")
cur.execute(""" cur.execute("""
UPDATE pipeline_runs UPDATE pipeline_runs
SET status = 'success', completed_at = ?, feedbacks_processed = ?, rules_generated = ?, rules_approved = ?, rules_deployed = ? SET status = 'success', completed_at = %s, feedbacks_processed = %s, rules_generated = %s, rules_approved = %s, rules_deployed = %s
WHERE run_id = ? WHERE run_id = %s
""", (datetime.datetime.now().isoformat(), len(feedbacks), rules_generated, rules_approved, rules_approved, run_id)) """, (datetime.datetime.now().isoformat(), len(feedbacks), rules_generated, rules_approved, rules_approved, run_id))
conn.commit() conn.commit()
return {"status": "success", "run_id": run_id, "feedbacks": len(feedbacks), "rules": rules_generated} return {"status": "success", "run_id": run_id, "feedbacks": len(feedbacks), "rules": rules_generated}
except Exception as e: except Exception as e:
logger.error(f"Pipeline error: {e}", exc_info=True) logger.error(f"Pipeline error: {e}", exc_info=True)
cur.execute("UPDATE pipeline_runs SET status = 'failed' WHERE run_id = ?", (run_id,)) cur.execute("UPDATE pipeline_runs SET status = 'failed' WHERE run_id = %s", (run_id,))
conn.commit() conn.commit()
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}
finally: finally:
...@@ -103,8 +104,9 @@ async def run_pipeline(): ...@@ -103,8 +104,9 @@ async def run_pipeline():
async def get_history(limit: int = 50): async def get_history(limit: int = 50):
conn = get_db() conn = get_db()
cur = conn.cursor() cur = conn.cursor()
cur.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT ?", (limit,)) cur.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT %s", (limit,))
rows = [dict(r) for r in cur.fetchall()] columns = [desc[0] for desc in cur.description] if cur.description else []
rows = [dict(zip(columns, r)) for r in cur.fetchall()]
conn.close() conn.close()
return {"runs": rows} return {"runs": rows}
...@@ -112,12 +114,13 @@ async def get_history(limit: int = 50): ...@@ -112,12 +114,13 @@ async def get_history(limit: int = 50):
async def get_history_detail(run_id: str): async def get_history_detail(run_id: str):
conn = get_db() conn = get_db()
cur = conn.cursor() cur = conn.cursor()
cur.execute("SELECT * FROM pipeline_runs WHERE run_id = ?", (run_id,)) cur.execute("SELECT * FROM pipeline_runs WHERE run_id = %s", (run_id,))
columns = [desc[0] for desc in cur.description] if cur.description else []
row = cur.fetchone() row = cur.fetchone()
conn.close() conn.close()
if not row: if not row:
raise HTTPException(status_code=404, detail="Run not found") raise HTTPException(status_code=404, detail="Run not found")
return dict(row) return dict(zip(columns, row))
@router.post("/rollback/{run_id}") @router.post("/rollback/{run_id}")
async def rollback_pipeline(run_id: str): async def rollback_pipeline(run_id: str):
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Manage Ultra Description API module
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
-- Dump for table: dashboard_canifa.activity_logs
-- Extracted rows: 0
-- (Empty Table)
-- Dump for table: dashboard_canifa.system_settings
-- Extracted rows: 0
-- (Empty Table)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment