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

feat: Full A-Z token optimization for AI Report Generator

parent 53e1bcd9
Pipeline #3386 failed with stage
"""
AI Feedback Classifier
Uses GPT-4.1-mini to classify user complaints into categories and severity.
Runs async in background — does not block user response.
"""
import json
import logging
from langchain_openai import ChatOpenAI
logger = logging.getLogger(__name__)
CLASSIFY_PROMPT = """Bạn là hệ thống phân loại phàn nàn khách hàng cho chatbot Canifa AI Stylist.
Phân loại phàn nàn sau đây của khách hàng.
## Phàn nàn:
"{comment}"
## Danh mục (chọn 1):
- thieu_du_lieu: Thiếu thông tin sản phẩm (size, màu, giá, mô tả...)
- du_lieu_sai: Thông tin sai (giá sai, tên sản phẩm sai, link sai...)
- bot_khong_hieu: Bot không hiểu ý khách hàng, trả lời lạc đề
- trai_nghiem_kem: Bot chậm, khó dùng, giao diện kém
- khac: Không thuộc các loại trên
## Mức độ nghiêm trọng:
- high: Ảnh hưởng trực tiếp đến quyết định mua hàng
- medium: Gây khó chịu nhưng không ảnh hưởng mua hàng
- low: Góp ý nhỏ, cải thiện trải nghiệm
## Trả về JSON (chỉ JSON, không giải thích):
{{"category": "...", "severity": "...", "summary": "tóm tắt ngắn gọn vấn đề"}}"""
async def classify_feedback(comment: str) -> dict:
"""
Classify a user complaint using GPT-4.1-mini.
Args:
comment: The user's complaint text
Returns:
dict with keys: category, severity, summary
Falls back to defaults on error.
"""
if not comment or not comment.strip():
return {
"category": "khac",
"severity": "low",
"summary": "Không có nội dung phàn nàn",
}
try:
llm = ChatOpenAI(
model="gpt-4.1-mini",
temperature=0,
max_tokens=200,
)
prompt = CLASSIFY_PROMPT.format(comment=comment)
response = await llm.ainvoke(prompt)
content = response.content.strip()
# Parse JSON from response (handle markdown code blocks)
if "```" in content:
content = content.split("```")[1]
if content.startswith("json"):
content = content[4:]
content = content.strip()
result = json.loads(content)
# Validate category
valid_categories = ["thieu_du_lieu", "du_lieu_sai", "bot_khong_hieu", "trai_nghiem_kem", "khac"]
if result.get("category") not in valid_categories:
result["category"] = "khac"
# Validate severity
valid_severities = ["low", "medium", "high"]
if result.get("severity") not in valid_severities:
result["severity"] = "medium"
if "summary" not in result:
result["summary"] = comment[:100]
logger.info(f"🤖 AI classified: {result['category']} / {result['severity']}")
return result
except Exception as e:
logger.warning(f"⚠️ AI classification failed: {e}")
return {
"category": "khac",
"severity": "medium",
"summary": comment[:100] if comment else "Classification error",
}
This diff is collapsed.
......@@ -54,3 +54,11 @@ class PostgresReadonly:
async with pool.acquire() as conn:
rows = await conn.fetch(sql)
return [dict(r) for r in rows]
@classmethod
async def execute_insert_async(cls, sql: str, *args) -> int:
"""Execute an INSERT query with args and return the new ID."""
pool = await cls._get_pool()
async with pool.acquire() as conn:
result = await conn.fetchval(sql, *args)
return result
This diff is collapsed.
......@@ -25,6 +25,7 @@ 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 api.report_html_route import router as report_html_router
from common.cache import redis_cache
from common.middleware import middleware_manager
from config import PORT
......@@ -113,6 +114,7 @@ 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)
app.include_router(report_html_router) # AI Report HTML Generator
if __name__ == "__main__":
......
......@@ -121,7 +121,7 @@
<div class="container">
<a href="ai-analytic.html" class="back-link">← Quay lại Report Agent</a>
<a href="ai-report.html" class="back-link">← Quay lại Report Agent</a>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- OVERVIEW -->
......@@ -357,7 +357,7 @@
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div style="text-align:center;padding:32px 0 20px;border-top:1px solid #E2E8F0">
<p style="font-size:14px;color:#64748B;margin-bottom:16px">Sẵn sàng tạo báo cáo?</p>
<a href="ai-analytic.html" style="display:inline-flex;align-items:center;gap:8px;padding:12px 28px;background:linear-gradient(135deg,#1E40AF,#4338CA);color:white;text-decoration:none;border-radius:12px;font-size:14px;font-weight:700;box-shadow:0 4px 14px rgba(67,56,202,.3);transition:all .2s" onmouseenter="this.style.transform='translateY(-1px)'" onmouseleave="this.style.transform='translateY(0)'">
<a href="ai-report.html" style="display:inline-flex;align-items:center;gap:8px;padding:12px 28px;background:linear-gradient(135deg,#1E40AF,#4338CA);color:white;text-decoration:none;border-radius:12px;font-size:14px;font-weight:700;box-shadow:0 4px 14px rgba(67,56,202,.3);transition:all .2s" onmouseenter="this.style.transform='translateY(-1px)'" onmouseleave="this.style.transform='translateY(0)'">
📋 Mở AI Report Agent →
</a>
</div>
......
This diff is collapsed.
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="ai-analytic.html" class="nav-item" onclick="navigateTo(this)">
<a data-page="ai-report.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>
......
# 📋 Hướng dẫn CSS Class cho AI sinh Report
> **Mục đích:** AI đọc file này → biết dùng class nào → sinh body HTML đúng format.
> CSS file: `report_template.css` (load qua `<link>`, KHÔNG thay đổi).
---
## 🏗️ Cấu trúc cơ bản
```
<body>
<!-- Nút in -->
<button class="print-btn no-print" onclick="window.print()">🖨 In / Lưu PDF</button>
<!-- Trang bìa -->
<div class="page">
<div class="cover">...</div>
</div>
<!-- Các trang nội dung (mỗi trang = 1 div.page.cp) -->
<div class="page cp">...</div>
<div class="page cp">...</div>
<!-- Chart.js script cuối body -->
<script>...</script>
</body>
```
---
## 📄 1. TRANG BÌA (Cover Page)
```html
<div class="page">
<div class="cover">
<div class="cover-stripe"></div> <!-- thanh navy trên cùng -->
<div class="cover-stripe-gold"></div> <!-- thanh vàng mỏng -->
<div class="cover-deco"></div> <!-- trang trí tròn (decorative) -->
<div class="cover-deco2"></div>
<div class="cover-inner">
<!-- Logo + Meta -->
<div class="cover-toprow">
<div class="org-logo">
<div class="org-badge">LOGO</div> <!-- badge 42x42pt -->
<div class="org-info">
<span class="org-name">Tên công ty</span>
<span class="org-dept">Phòng ban</span>
</div>
</div>
<div class="doc-meta">
Số hiệu: <strong>MÃ-SỐ</strong><br>
Phiên bản: <strong>1.0</strong><br>
Phân loại: <em>Nội bộ</em>
</div>
</div>
<!-- Tiêu đề chính -->
<div class="cover-hero">
<div class="cover-tag">Loại báo cáo</div>
<h1 class="cover-title">Tiêu đề<br>Báo cáo</h1>
<p class="cover-subtitle">Mô tả ngắn</p>
<div class="cover-divider"></div>
<!-- 4 ô thông tin -->
<div class="cover-metarow">
<div class="cover-metaitem">
<span class="cover-metalabel">Label</span>
<span class="cover-metaval">Value</span>
</div>
<!-- ... thêm 3 ô nữa -->
</div>
</div>
<div class="cover-footer">
<span class="cover-footer-note">Ghi chú bảo mật</span>
<span class="cover-stamp">BẢO MẬT NỘI BỘ</span>
</div>
</div>
</div>
</div>
```
---
## 📃 2. TRANG NỘI DUNG
```html
<div class="page cp">
<!-- Header chạy -->
<div class="rh">
<span class="rh-left"><span class="rh-dot"></span>Tiêu đề báo cáo</span>
<span class="rh-right">Trang X / Y</span>
</div>
<!-- Nội dung ở đây -->
<!-- Footer trang -->
<div class="pf">
<span class="pf-left">Tên công ty · Bảo mật nội bộ</span>
<span class="pf-center">MÃ-SỐ</span>
<span class="pf-right">X / Y</span>
</div>
</div>
```
---
## 🧩 3. CÁC COMPONENT
### Heading
```html
<h1>Tiêu đề lớn</h1>
<h2 data-n="1">Heading cấp 2 (số tự động hiện trong badge)</h2>
<h3>Heading cấp 3 (in nghiêng)</h3>
```
### Đoạn văn
```html
<p>Nội dung thường</p>
<p class="lead">Đoạn mở đầu (to hơn, nhạt hơn)</p>
```
### Executive Summary Box
```html
<div class="esbox">
<div class="esbox-label">Executive Summary</div>
<p>Nội dung tóm tắt...</p>
</div>
```
### KPI Cards (grid 4 cột)
```html
<div class="kpi-grid">
<div class="kpi" style="--kpi-accent:var(--blue-mid)">
<span class="kpi-label">TÊN CHỈ SỐ</span>
<span class="kpi-value">142,6<span class="kpi-unit">tỷ</span></span>
<span class="kpi-change up">↑ 18,4% YoY</span> <!-- up = xanh, dn = đỏ, nt = xám -->
</div>
<!-- ... thêm 3 KPI nữa -->
</div>
```
**Màu accent KPI:** `var(--blue-mid)`, `var(--green)`, `var(--gold)`, `var(--red)`
### Biểu đồ (Chart.js)
```html
<div class="fig">
<div class="fig-header">
<span class="fig-num">Hình X.Y</span>
<span class="fig-title">Tiêu đề biểu đồ</span>
</div>
<div class="fig-wrap"><canvas id="c-unique-id" height="175"></canvas></div>
<div class="fig-cap"><strong>Nguồn:</strong> Mô tả nguồn dữ liệu.</div>
</div>
```
### Bảng dữ liệu
```html
<div class="tbl-wrap">
<div class="tbl-cap">Bảng X.Y — Tiêu đề bảng</div>
<table>
<thead><tr>
<th>Cột 1</th>
<th class="r">Cột phải</th> <!-- class="r" = căn phải -->
</tr></thead>
<tbody>
<tr>
<td>Dữ liệu</td>
<td class="r">123,4</td>
</tr>
<tr class="trow-total"> <!-- dòng tổng -->
<td>Tổng</td>
<td class="r">456,7</td>
</tr>
</tbody>
</table>
</div>
```
### Badge (trong bảng)
```html
<span class="badge up">↑ 18,4%</span> <!-- xanh lá -->
<span class="badge dn">↓ 4,3%</span> <!-- đỏ -->
<span class="badge nt">— 0%</span> <!-- xám -->
```
### Callout / Note
```html
<!-- 3 loại: info (xanh), warn (vàng), success (xanh lá) -->
<div class="note info">
<div class="note-icon">i</div>
<p><strong>Lưu ý:</strong> Nội dung ghi chú.</p>
</div>
<div class="note warn">
<div class="note-icon">!</div>
<p><strong>Cảnh báo:</strong> Nội dung cảnh báo.</p>
</div>
<div class="note success">
<div class="note-icon"></div>
<p><strong>Tích cực:</strong> Nội dung tích cực.</p>
</div>
```
### Mục lục (TOC)
```html
<div class="toc-item">
<span class="toc-num">1.</span>
<span class="toc-text">Tên mục</span>
<span class="toc-dots"></span>
<span class="toc-pg">3</span>
</div>
<div class="toc-item toc-sub"> <!-- mục con: thêm class toc-sub -->
<span class="toc-num">1.1</span>
<span class="toc-text">Mục con</span>
<span class="toc-dots"></span>
<span class="toc-pg">3</span>
</div>
```
### Khuyến nghị
```html
<div class="rec-item">
<span class="rec-n">1</span>
<span class="rec-text">Nội dung khuyến nghị.</span>
</div>
```
### Khối chữ ký
```html
<div class="sigblock">
<div class="sig-item">
<div class="sig-label">Chức danh</div>
<div class="sig-line" style="height:14mm"></div>
<div class="sig-name">Họ Tên</div>
<div class="sig-role">Vai trò</div>
</div>
<!-- ... thêm 2 sig-item nữa (3 cột) -->
</div>
```
### Chú thích cuối trang
```html
<hr class="fn-rule">
<p class="fn">¹ Nội dung chú thích.</p>
```
---
## 🎨 4. CHART.JS CONFIG (AI cũng sinh phần này)
**Boilerplate helpers** (copy nguyên):
```javascript
const F = {family:"'Inter',sans-serif",size:9}
const TIP = {backgroundColor:"#0F2044",titleColor:"#93C5FD",bodyColor:"#F1F5F9",padding:11,cornerRadius:6,titleFont:{...F,size:9},bodyFont:{...F,size:11,weight:"700"}}
const AX = (cb)=>({grid:{color:"rgba(0,0,0,.05)"},border:{display:false},ticks:{color:"#6B7280",font:F,...(cb?{callback:cb}:{})}})
const AXX = {grid:{display:false},border:{display:false},ticks:{color:"#6B7280",font:F}}
```
**Bảng màu chart** (dùng theo thứ tự):
| Biến | Hex | Dùng cho |
|------|-----|----------|
| `--chart-1` | `#1A4A8C` | Primary (navy blue) |
| `--chart-2` | `#C8860A` | Secondary (gold) |
| `--chart-3` | `#0D6E4F` | Tertiary (green) |
| `--chart-4` | `#7C3AED` | Quaternary (purple) |
| `--chart-5` | `#B91C1C` | Quinary (red) |
| `--chart-6` | `#0891B2` | Senary (cyan) |
**Các loại chart thường dùng:**
| Loại | Chart.js type | Khi nào dùng |
|------|---------------|-------------|
| Bar (dọc) | `bar` | So sánh theo tháng/quý |
| Bar (ngang) | `bar` + `indexAxis:"y"` | Ranking/tăng trưởng |
| Donut | `doughnut` + `cutout:"65%"` | Cơ cấu/tỷ trọng |
| Line | `line` | Xu hướng theo thời gian |
| Grouped bar | `bar` + 2 datasets | So sánh YoY |
---
## 📐 5. LAYOUT PATTERNS
### 2 cột (chart cạnh nhau)
```html
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8mm;margin:4mm 0">
<div class="fig">...</div>
<div class="fig">...</div>
</div>
```
### 2 cột không đều (1.2:1)
```html
<div style="display:grid;grid-template-columns:1.2fr 1fr;gap:8mm;margin:5mm 0 3mm">
<div class="fig">...</div>
<div class="fig">...</div>
</div>
```
---
## ⚡ 6. CHECKLIST CHO AI KHI SINH REPORT
- [ ] Bắt đầu bằng `<button class="print-btn">`
- [ ] Trang bìa: `.page > .cover`
- [ ] Mỗi trang nội dung: `.page.cp`
- [ ] Mỗi trang có `.rh` (header) và `.pf` (footer)
- [ ] `<h2 data-n="X">` — số X tự hiện trong badge navy
- [ ] KPI dùng `.kpi-grid` + 4 `.kpi` cards
- [ ] Chart: `<canvas id="c-xxx">` + script cuối body
- [ ] Badge trong bảng: `.badge.up` / `.badge.dn` / `.badge.nt`
- [ ] Callout dùng `.note.info` / `.note.warn` / `.note.success`
- [ ] Cuối report: `.sigblock` + `.fn` + `.fn-rule`
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