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

update: Refactor Daily Session logic, History API with URL-based Guest identity, and API Docs

parent 355c8bdd
# Canifa Chatbot API (Simplified)
Base URL: `http://172.16.2.207:5000`
---
## 1. Chat (Gửi tin nhắn)
**POST** `/api/agent/chat`
### Request
#### Guest (Chưa login)
```json
{
"user_query": "Tìm áo thun nam",
"device_id": "my-device-123"
}
```
#### User (Đã login)
```json
Headers: Authorization: Bearer <token>
{
"user_query": "Tìm áo thun nam",
"device_id": "my-device-123"
}
```
### Response
```json
{
"status": "success",
"ai_response": "Shop có mẫu áo thun này...",
"product_ids": [
{
"sku": "8TS24W001",
"name": "Áo thun nam Basic",
"price": 250000,
"sale_price": 199000,
"url": "https://canifa.com/...",
"thumbnail_image_url": "https://..."
}
],
"limit_info": { "limit": 10, "used": 1, "remaining": 9 }
}
```
### Error Response (500)
Trong trường hợp lỗi hệ thống (DB, LLM...), API sẽ trả về HTTP 500 kèm body:
```json
{
"status": "error",
"error_code": "SYSTEM_ERROR",
"message": "Oops 😥 Hiện Canifa-AI chưa thể xử lý yêu cầu của bạn..."
}
```
### Error Response (429) - Rate Limit Exceeded
Khi user/guest vượt quá giới hạn tin nhắn cho phép:
**Trường hợp 1: Guest hết lượt (Cần login)**
```json
{
"status": "error",
"error_code": "GUEST_LIMIT_EXCEEDED",
"message": "Bạn đã sử dụng hết tin nhắn hôm nay. Đăng nhập ngay để dùng tiếp: https://canifa.com/login",
"require_login": true,
"limit_info": {
"limit": 10,
"used": 10,
"remaining": 0,
"reset_seconds": 3600
}
}
```
**Trường hợp 2: User hết lượt (Hoặc Guest đạt Hard Limit)**
```json
{
"status": "error",
"error_code": "USER_LIMIT_EXCEEDED",
"message": "Bạn đã sử dụng hết tin nhắn hôm nay. Vui lòng quay lại vào hôm sau để dùng tiếp!",
"require_login": false,
"limit_info": { ... }
}
```
---
## 2. History (Lấy lịch sử)
**GET** `/api/history/{your_device_id}?limit=20&before_id=105`
### Query Parameters
| Param | Type | Description |
| :--- | :--- | :--- |
| `limit` | int | Số tin nhắn (Default: 50) |
| `before_id` | int | ID tin nhắn cuối của trang trước (để load thêm) |
### Request
**Guest:**
`/api/history/my-device-123?limit=20`
**User:**
`/api/history/my-device-123?limit=20` (Param URL vẫn giữ là device_id cho tiện FE)
Header: `Authorization: Bearer <token>`
*(Backend sẽ tự ưu tiên lấy User ID từ Token để truy vấn lịch sử)*
### Response
```json
{
"data": [
{
"id": 105,
"message": "...", // JSON String
"is_human": false,
"timestamp": "..."
}
],
"next_cursor": 104 // Dùng ID này cho `before_id` tiếp theo
}
```
---
## 3. Reset (Xóa và tạo mới)
**POST** `/api/history/archive`
### Request
Gửi Header `device_id` (Guest) hoặc `Authorization` (User).
Body rỗng `{}`.
### Response
```json
{
"status": "success",
"success": true,
"message": "Archived successfully",
"new_key": "my-device-123_archived_..."
}
```
......@@ -87,7 +87,7 @@ async def chat_controller(
messages = [
HumanMessage(content=m["message"]) if m["is_human"] else AIMessage(content=m["message"])
for m in history_dicts
]
][::-1] # Reverse to chronological order (Oldest -> Newest)
# Prepare State
initial_state: AgentState = {
......
......@@ -97,11 +97,31 @@ query = "áo len giá dưới 500k" # ❌ Có giá trong query
---
# TỰ SUY LUẬN THÔNG MINH
# TỰ SUY LUẬN & GIỮ NGỮ CẢNH (CONTEXT)
Bot phải **phân tích ngữ cảnh** tự động:
Bot phải **đọc kỹ lịch sử chat** để duy trì mạch hội thoại:
### Case 1: "Áo cho đàn ông đi chơi"
## Nguyên tắc "Kế thừa Lịch sử":
Khi khách hỏi vắn tắt câu sau, hãy **GIỮ LẠI** thông tin cũ (Giới tính, Tuổi, Loại SP) từ câu trước.
### Ví dụ 1: Kế thừa ngữ cảnh (Follow-up)
**Lịch sử:**
- User: "Tìm quần jeans cho bé gái 10 tuổi"
-> Context cũ: `female`, `kid`, `jeans`
**Hiện tại:**
- User: "Thế còn quần nỉ?"
-> **Suy luận:** Khách vẫn tìm cho **bé gái 10 tuổi**, chỉ đổi loại sang **Quần nỉ**.
-> **Query:**
```
product_name: Quần nỉ
gender_by_product: female
age_by_product: kid
```
*(Nếu không kế thừa -> Bot sẽ tìm quần nỉ cho người lớn -> SAI)*
### Ví dụ 2: Suy luận từ nhu cầu (Case mới)
"Áo cho đàn ông đi chơi"
→ Suy luận:
- Đàn ông → `male` + `adult`
- Đi chơi → `casual`
......@@ -124,10 +144,11 @@ style: casual
"""
```
### Case 2: "28 tuổi nữ, văn phòng + đi chơi, HN 12-15°C"
### Ví dụ 3: Case phức tạp
"28 tuổi nữ, văn phòng + đi chơi, HN 12-15°C"
→ Suy luận:
- Lạnh → Cần giữ ấm
- Văn phòng + đi chơi → Đa năng
- Lạnh → Cần giữ ấm (Đông)
- Văn phòng + đi chơi → Formal/Casual
- Nữ 28 tuổi → `female` + `adult`
→ Sinh 3 query:
......@@ -154,13 +175,7 @@ style: formal
"""
```
### Case 3: "2tr cho 5 người: 2 bé trai 8-10 tuổi, 1 bé gái 5 tuổi, nam 1m78, nữ 1m62"
→ Suy luận:
- Ngân sách: 2,000,000 / 5 = ~400k/người
- Cần 5 query riêng cho từng người
```python
# Query 1: Bé trai 8 tuổi
### Ví dụ 4: Mua nhiều người
query = """
product_name: Quần áo
gender_by_product: male
......
......@@ -27,13 +27,15 @@ async def canifa_knowledge_search(query: str) -> str:
4. KHÁCH HÀNG THÂN THIẾT (KHTT): Điều kiện đăng ký thành viên, các hạng thẻ (Green, Silver, Gold, Diamond), quyền lợi tích điểm, thẻ quà tặng.
5. HỖ TRỢ & FAQ: Giải đáp thắc mắc thường gặp, chính sách bảo mật, thông tin liên hệ văn phòng, tuyển dụng.
6. TRA CỨU SIZE (BẢNG KÍCH CỠ): Hướng dẫn chọn size chuẩn cho nam, nữ, trẻ em dựa trên chiều cao, cân nặng.
7. GIẢI NGHĨA TỪ VIẾT TẮT: Tự động hiểu các từ viết tắt phổ biến của khách hàng (ví dụ: 'ct' = 'chương trình khuyến mãi/ưu đãi', 'khtt' = 'khách hàng thân thiết', 'store' = 'cửa hàng', 'đc' = 'địa chỉ').
Ví dụ các câu hỏi phù hợp:
- 'Bên bạn đang có ct gì không?' (Hiểu là: Chương trình khuyến mãi)
- 'Canifa ở Cầu Giấy địa chỉ ở đâu?'
- 'Chính sách đổi trả hàng trong bao nhiêu ngày?'
- 'Làm sao để lên hạng thẻ Gold?'
- 'Cho mình xem bảng size áo nam.'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Canifa thành lập năm nào?'
"""
logger.info(f"🔍 [Semantic Search] Brand Knowledge query: {query}")
......
......@@ -39,8 +39,14 @@ async def get_chat_history(request: Request, identity_key: str, limit: int | Non
try:
# Tự động resolve identity từ middleware
identity = get_user_identity(request)
resolved_key = identity.history_key # user_id nếu login, device_id nếu không
# Nếu đã login -> Dùng user_id
if identity.is_authenticated:
resolved_key = identity.history_key
else:
# Nếu chưa login (Guest) -> Dùng identity_key từ URL
resolved_key = identity_key
logger.info(f"GET History: URL key={identity_key} -> Resolved key={resolved_key}")
manager = await get_conversation_manager()
......@@ -80,7 +86,6 @@ class ArchiveResponse(BaseModel):
success: bool
message: str
new_key: str
remaining_resets: int
@router.post("/api/history/archive", summary="Archive Chat History", response_model=ArchiveResponse)
......@@ -90,19 +95,22 @@ async def archive_chat_history(request: Request):
Giới hạn 5 lần/ngày.
"""
try:
# Tự động resolve identity
# NOTE: Với Reset, ta extract lại device_id từ body nếu có (cho trường hợp Guest bấm reset)
try:
req_json = await request.json()
body_device_id = req_json.get("device_id")
except:
body_device_id = None
identity = get_user_identity(request)
identity_key = identity.history_key
# if not identity.is_authenticated:
# return JSONResponse(
# status_code=403,
# content={
# "status": "error",
# "error_code": "LOGIN_REQUIRED",
# "message": "Tính năng chỉ dành cho thành viên đã đăng nhập.",
# "require_login": True
# }
# )
# Nếu chưa login mà có body_device_id -> ưu tiên dùng nó làm key
if not identity.is_authenticated and body_device_id:
logger.info("Archive: Using device_id from Body for Guest")
identity_key = body_device_id
else:
identity_key = identity.history_key
# Check reset limit
can_reset, usage, remaining = await reset_limit_service.check_limit(identity_key)
......@@ -126,8 +134,7 @@ async def archive_chat_history(request: Request):
"status": "success",
"success": True,
"message": "History archived successfully",
"new_key": new_key,
"remaining_resets": remaining - 1 if remaining > 0 else 0
"new_key": new_key
}
except Exception as e:
logger.error(f"Error archiving history: {e}")
......
......@@ -120,13 +120,14 @@ class ConversationManager:
max_retries = 3
for attempt in range(max_retries):
try:
today = datetime.now().date()
query = f"""
SELECT message, is_human, timestamp, id
FROM {self.table_name}
WHERE identity_key = %s
AND DATE(timestamp) = DATE(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')
AND DATE(timestamp) = %s
"""
params = [identity_key]
params = [identity_key, today]
if before_id:
query += " AND id < %s"
......@@ -191,8 +192,10 @@ class ConversationManager:
"""
try:
timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
# Format: user123_archived_20231027_103045
new_key = f"{identity_key}_archived_{timestamp_suffix}"
today = datetime.now().date()
pool = await self._get_pool()
async with pool.connection() as conn:
async with conn.cursor() as cursor:
......@@ -202,9 +205,9 @@ class ConversationManager:
UPDATE {self.table_name}
SET identity_key = %s
WHERE identity_key = %s
AND DATE(timestamp) = DATE(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')
AND DATE(timestamp) = %s
""",
(new_key, identity_key)
(new_key, identity_key, today)
)
await conn.commit()
......
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