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

feat: add AI analytic dashboard + report agent

- Add AI analytic dashboard (ai-analytic.html)
- Add report agent route and prompt
- Add langfuse analytics module
- Update middleware and server config
parent 7fd76787
Pipeline #3380 failed with stage
This diff is collapsed.
This diff is collapsed.
......@@ -129,7 +129,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
# ⚠️ Skip body reading for SSE/streaming endpoints — reading the
# body inside BaseHTTPMiddleware corrupts the receive chain and
# causes "Unexpected message received: http.request" errors.
SSE_PATHS = {"/api/sql-dashboard", "/api/sql-chat/stream"}
SSE_PATHS = {"/api/sql-dashboard", "/api/sql-chat/stream", "/api/report-agent"}
device_id = ""
if method in ["POST", "PUT", "PATCH"] and path not in SSE_PATHS:
try:
......
"""
Report Agent Prompts — ReAct loop with reflect for AI Report generation.
Imported by api/report_agent_route.py
"""
from prompts.dashboard_prompt import STARROCKS_TABLE, POSTGRES_TABLE, DB_SCHEMA
# ═══════════════════════════════════════════════════════════════════
# LANGFUSE TABLE (in StarRocks)
# ═══════════════════════════════════════════════════════════════════
LANGFUSE_TABLE = "analytic.chatbot_rsa_trace_event_detail"
LANGFUSE_SCHEMA = f"""
## Table: {LANGFUSE_TABLE} (StarRocks — Langfuse Analytics)
Columns:
- trace_id (VARCHAR), project_name (VARCHAR), org_name (VARCHAR)
- session_id (VARCHAR), customer_id (VARCHAR), device_id (VARCHAR)
- trace_latency (DECIMAL — seconds), user_latency (DECIMAL — seconds)
- input_cost (DECIMAL — USD), output_cost (DECIMAL — USD), total_cost (DECIMAL — USD)
- model_name (VARCHAR — e.g. 'gpt-5.2-codex', 'gemini-3.1-flash-lite')
- total_obs (BIGINT), total_obs_error (BIGINT), nb_generation_error (BIGINT)
- is_guest (TINYINT — 1/0), is_user (TINYINT — 1/0)
- traced_at (DATETIME), created_at (DATETIME), updated_at (DATETIME)
"""
# ═══════════════════════════════════════════════════════════════════
# AGENT PROMPT — ReAct loop with reflection
# ═══════════════════════════════════════════════════════════════════
AGENT_PROMPT = f"""You are an AI Data Analyst Agent for Canifa (Vietnamese fashion brand).
You work in a ReAct loop: THINK → ACT → OBSERVE → REFLECT → (repeat or finish).
## AVAILABLE TOOLS:
1. **sql_langfuse** — Query `{LANGFUSE_TABLE}` on StarRocks
Use for: chatbot usage, costs, latency, models, errors, users
{LANGFUSE_SCHEMA}
2. **sql_starrocks** — Query `{STARROCKS_TABLE}` on StarRocks
Use for: product analytics, sales, categories. NEVER SELECT 'vector'/'embedding' columns.
Columns: product_name, sale_price, original_price, quantity_sold, gender_by_product, product_line_vn, master_color, material, season, is_new_product, etc.
3. **sql_postgres** — Query `{POSTGRES_TABLE}` on PostgreSQL
Use for: chat history, user messages, conversation analysis.
Columns: identity_key, message, is_human, timestamp
4. **calculator** — Pure math only (digits + -, *, /, parentheses). No text in expression.
## YOUR TASK:
Given the user's question, iteratively gather data until you have ENOUGH for a comprehensive report.
## RESPONSE FORMAT (JSON):
At each step, respond with ONE of these:
### Option 1: Execute tools (gather more data)
```json
{{
"action": "execute",
"thinking": "What I need to find out and why",
"tools": [
{{"name": "tool_name", "params": {{"sql": "SELECT ..."}}, "purpose": "Why this query"}}
]
}}
```
### Option 2: Reflect on collected data
```json
{{
"action": "reflect",
"thinking": "Assessment of data collected so far",
"data_sufficient": true/false,
"missing": ["What data is still missing (if any)"],
"quality_issues": ["Data quality concerns (if any)"],
"next_tools": [
{{"name": "tool_name", "params": {{"sql": "SELECT ..."}}, "purpose": "Why this additional query"}}
]
}}
```
- If data_sufficient=true AND missing=[] → agent will proceed to write report
- If data_sufficient=false → next_tools will be executed, then another reflect cycle
### Option 3: Write the final report
```json
{{
"action": "write_report"
}}
```
(Only output this when told all data is ready)
## RULES:
- Start with 2-4 diverse queries covering different aspects of the user's question
- After seeing results, REFLECT: is the data enough? Good quality? Need deeper analysis?
- If data has gaps, errors, or insufficient depth → add more targeted queries
- Maximum 3 reflect cycles (to prevent infinite loops)
- For calculator: expression must contain ONLY digits and math operators
- All SQL must be SELECT-only
- Be thorough but efficient — don't over-query
RESPOND WITH RAW JSON ONLY. No markdown.
"""
# ═══════════════════════════════════════════════════════════════════
# WRITER — Generates the full report JSON from real data
# ═══════════════════════════════════════════════════════════════════
WRITER_PROMPT = """You are an expert business analyst and report writer for Canifa (Vietnamese fashion brand).
You have received REAL DATA from various data tools. Write a comprehensive analytical report.
## CRITICAL: USE REAL DATA ONLY
- All numbers, charts, and tables MUST come from the provided tool results
- NEVER fabricate or estimate numbers that aren't in the data
- If data is missing for a section, note it honestly
## REPORT JSON SCHEMA:
{
"title": "string — full report title",
"subtitle": "string — descriptive subtitle",
"organization": "string — e.g. 'Phòng Phân tích AI — Canifa'",
"period": "string — reporting period",
"prepared_by": "string — 'AI Report Agent · Canifa AI Platform'",
"executive_summary": "string — 3–5 sentences summarizing key findings from REAL data",
"highlights": [ { "label": "string", "value": "string (from data)", "trend": "up|down|neutral" } ],
"sections": [ Section ],
"conclusion": "string — 2–4 sentences based on findings",
"recommendations": [ "string — actionable, based on data" ]
}
Section = {
"id": "string",
"number": "number (1, 2, 3...)",
"title": "string",
"paragraphs": [ "string — analytical text using REAL numbers from data" ],
"chart": Chart | null,
"table": Table | null,
"callout": { "type": "info|warning|success", "text": "string" } | null
}
Chart = {
"type": "bar|line|area|donut|hbar",
"title": "string",
"caption": "string — 1 sentence explaining what the chart shows",
"labels": ["string"],
"datasets": [ { "label": "string", "values": [number] } ],
"segments": [ { "name": "string", "value": number } ]
}
Table = {
"title": "string",
"caption": "string",
"columns": ["string"],
"rows": [["string"]]
}
## CONTENT RULES:
- Write each paragraph as analytical text (3–5 sentences), referencing REAL data
- 4–6 sections, each focused on one aspect
- Each section should have 2–3 paragraphs
- Put a chart OR table in most sections (not both unless very relevant)
- highlights: 4–6 key KPIs from the data
- recommendations: 4–6 actionable points based on findings
- All text in Vietnamese
- Chart data MUST match the tool results — do NOT invent numbers
- For donut charts use "segments", for other types use "labels" + "datasets"
RESPOND WITH RAW JSON ONLY. No markdown fences.
"""
......@@ -24,6 +24,7 @@ from api.experiment_links_route import router as experiment_links_router
from api.product_route import router as product_router
from api.sql_chat_route import router as sql_chat_router
from api.cache_route import router as cache_router
from api.report_agent_route import router as report_agent_router
from common.cache import redis_cache
from common.middleware import middleware_manager
from config import PORT
......@@ -64,8 +65,7 @@ async def startup_event():
@app.get("/")
async def root():
return RedirectResponse(url="/static/dashboard.html")
return RedirectResponse(url="/static/main.html")
@app.get("/health")
async def health():
......@@ -88,8 +88,8 @@ async def serve_static(file_path: str):
# =====================================================================
middleware_manager.setup(
app,
enable_auth=True,
enable_rate_limit=True,
enable_auth=False,
enable_rate_limit=False,
enable_cors=True,
cors_origins=["*"],
)
......@@ -112,6 +112,7 @@ app.include_router(experiment_links_router) # Experiment links sidebar
app.include_router(product_router) # Product performance dashboard
app.include_router(sql_chat_router) # AI Data Analyst (Text-to-SQL)
app.include_router(cache_router) # Cache management dashboard
app.include_router(report_agent_router) # AI Report Agent (SSE)
if __name__ == "__main__":
......
This diff is collapsed.
This diff is collapsed.
......@@ -48,7 +48,7 @@ body{margin:0;display:flex;min-height:100vh}
<span class="nav-icon">🏷️</span>
<span>Product Perf.</span>
</a>
<a data-page="sql-chart.html" class="nav-item" onclick="navigateTo(this)">
<a data-page="ai-analytic.html" class="nav-item" onclick="navigateTo(this)">
<span class="nav-icon">📊</span>
<span>AI Data Analyst</span>
<span class="nav-badge badge-beta">NEW</span>
......@@ -94,7 +94,7 @@ body{margin:0;display:flex;min-height:100vh}
<span>Feedback Demo</span>
<span class="nav-badge badge-new">NEW</span>
</a>
<a data-page="index.html" class="nav-item" onclick="navigateTo(this)">
<a data-page="http://172.16.2.210:5006/static/index.html" class="nav-item" onclick="navigateTo(this)">
<span class="nav-icon">💬</span>
<span>Chatbot (Dev)</span>
<span class="nav-badge badge-beta">DEV</span>
......@@ -135,7 +135,8 @@ body{margin:0;display:flex;min-height:100vh}
function navigateTo(el) {
const page = el.getAttribute('data-page');
if (!page) return;
document.getElementById('contentFrame').src = '/static/' + page;
const src = page.startsWith('http') ? page : '/static/' + page;
document.getElementById('contentFrame').src = src;
// Update active state
document.querySelectorAll('#mainSidebar .nav-item').forEach(n => n.classList.remove('active'));
el.classList.add('active');
......@@ -151,7 +152,8 @@ function navigateTo(el) {
const params = new URLSearchParams(window.location.search);
const page = params.get('page');
if (page) {
document.getElementById('contentFrame').src = '/static/' + page;
const src = page.startsWith('http') ? page : '/static/' + page;
document.getElementById('contentFrame').src = src;
// Highlight active nav
document.querySelectorAll('#mainSidebar .nav-item[data-page]').forEach(el => {
if (el.getAttribute('data-page') === page) {
......@@ -170,7 +172,8 @@ function navigateTo(el) {
// ═══ HANDLE BACK/FORWARD ═══
window.addEventListener('popstate', function(e) {
if (e.state && e.state.page) {
document.getElementById('contentFrame').src = '/static/' + e.state.page;
const src = e.state.page.startsWith('http') ? e.state.page : '/static/' + e.state.page;
document.getElementById('contentFrame').src = src;
document.querySelectorAll('#mainSidebar .nav-item[data-page]').forEach(el => {
el.classList.toggle('active', el.getAttribute('data-page') === e.state.page);
});
......
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