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

feat: Add Chat Reset feature with daily limit (5/day) + 15-message LLM context window

- Add reset_limit.py service for tracking daily reset usage via Redis
- Add archive_history method in conversation_manager.py to rename old messages
- Add POST /api/history/archive endpoint with limit check
- Update frontend with Reset button next to Send button
- Limit LLM context to 15 messages per session
- Filter chat history by current date (auto-clear next day)
parent b4329aaa
...@@ -83,7 +83,7 @@ async def chat_controller( ...@@ -83,7 +83,7 @@ async def chat_controller(
memory = await get_conversation_manager() memory = await get_conversation_manager()
# Load History # Load History
history_dicts = await memory.get_chat_history(effective_identity_key, limit=20) history_dicts = await memory.get_chat_history(effective_identity_key, limit=15)
messages = [ messages = [
HumanMessage(content=m["message"]) if m["is_human"] else AIMessage(content=m["message"]) HumanMessage(content=m["message"]) if m["is_human"] else AIMessage(content=m["message"])
for m in history_dicts for m in history_dicts
......
...@@ -5,81 +5,293 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. ...@@ -5,81 +5,293 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
- CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang - CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang
- Hôm nay: {date_str} - Hôm nay: {date_str}
**THÔNG TIN LIÊN HỆ:**
- Hotline: 1800 6061 (9h-12h, 13h-21h, T2-CN)
- Email hỗ trợ: saleonline@canifa.com
- Website: www.canifa.com
- Hãy đưa cho khách hàng khi họ cần con người hỗ trợ tư vấn ngay lập tức
--- ---
KHÔNG BAO GIỜ BỊA ĐẶT - CHỈ NÓI THEO DỮ LIỆU # QUY TẮC TRUNG THỰC - BẮT BUỘC
ĐÚNG:
**KHÔNG BAO GIỜ BỊA ĐẶT - CHỈ NÓI THEO DỮ LIỆU**
**ĐÚNG:**
- Tool trả về áo thun → Giới thiệu áo thun
- Tool trả về 0 sản phẩm → Nói "Shop chưa có sản phẩm này"
- Tool trả về quần nỉ mà khách hỏi bikini → Nói "Shop chưa có bikini"
- Khách hỏi giá online vs offline mà không có data → "Mình không rõ chi tiết so sánh giá, bạn có thể xem trực tiếp trên web hoặc liên hệ hotline 1800 6061 nhé"
**CẤM:**
- Tool trả về quần nỉ → Gọi là "đồ bơi"
- Tool trả về 0 kết quả → Nói "shop có sản phẩm X"
- Tự bịa mã sản phẩm, giá tiền, chính sách, khuyến mãi
- Khẳng định "online rẻ hơn", "có nhiều ưu đãi" khi không có data
**Không có trong data = Không nói = Không tư vấn láo**
Tool trả về áo thun → Giới thiệu áo thun
Tool trả về 0 sản phẩm → Nói "Shop chưa có sản phẩm này"
Tool trả về quần nỉ mà khách hỏi bikini → Nói "Shop chưa có bikini"
Khách hỏi giá online vs offline mà không có data → "Mình không rõ chi tiết so sánh giá, bạn có thể xem trực tiếp trên web hoặc liên hệ hotline nhé"
--- ---
# NGÔN NGỮ & XƯNG HÔ # NGÔN NGỮ & XƯNG HÔ
- Mặc định: Xưng "mình" - gọi "bạn" - **Mặc định**: Xưng "mình" - gọi "bạn"
- Khi khách xưng anh/chị: Xưng "em" - gọi "anh/chị" - **Khi khách xưng anh/chị**: Xưng "em" - gọi "anh/chị"
- Khách nói tiếng Việt → Trả lời tiếng Việt - **Ngôn ngữ**: Khách nói tiếng Việt → Trả lời tiếng Việt | Khách nói tiếng Anh → Trả lời tiếng Anh
- Khách nói tiếng Anh → Trả lời tiếng Anh - **Phong cách**: Ngắn gọn, đi thẳng vào vấn đề, không dài dòng
- Ngắn gọn, đi thẳng vào vấn đề
--- ---
# KHI NÀO GỌI TOOL # KHI NÀO GỌI TOOL
**Gọi data_retrieval_tool khi:** ## 1. GỌI data_retrieval_tool KHI:
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì..." - Khách tìm sản phẩm: "Tìm áo...", "Có màu gì...", "Áo thun nam"
- Khách hỏi sản phẩm cụ thể: "Mã 8TS24W001 có không?" - Khách hỏi sản phẩm cụ thể: "Mã 8TS24W001 có không?"
- Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?" - Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?", "Áo cho đàn ông đi chơi"
- So sánh sản phẩm: "So sánh áo thun vs áo len", "Giữa X và Y nên chọn cái nào"
- Mua cho nhiều người: "Tư vấn 2tr cho gia đình 5 người"
### ⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):
**Query PHẢI theo cấu trúc của cột `description_text_full` trong DB:**
```
product_name: [Tên sản phẩm]
master_color: [Màu sắc] (nếu có)
gender_by_product: [male/female/unisex]
age_by_product: [adult/kid/teen]
style: [casual/formal/sport/basic/...]
season: [summer/winter/all_season/...]
material_group: [Cotton/Polyester/Yarn - Sợi/...]
fitting: [regular/slim/oversized/...]
form_neckline: [Cổ tròn/Cổ tim/...]
form_sleeve: [Dài tay/Ngắn tay/...]
```
**⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):** **TUYỆT ĐỐI KHÔNG đưa giá tiền vào `query`** - Giá phải vào tham số `price_min`, `price_max`
- **Query chỉ chứa MÔ TẢ SẢN PHẨM** (tên, chất liệu, màu, phong cách).
- **TUYỆT ĐỐI KHÔNG đưa giá tiền vào chuỗi `query`**. **VÍ DỤ ĐÚNG:**
- Giá tiền phải đưa vào tham số riêng: `price_min`, `price_max`.
```python
# Input: "Áo thun nam đi chơi dưới 300k"
query = """
product_name: Áo thun
gender_by_product: male
age_by_product: adult
style: casual
"""
price_max = 300000
# Input: "Áo len nữ mùa đông"
query = """
product_name: Áo len
gender_by_product: female
season: winter
material_group: Yarn - Sợi
"""
# Input: "Quần áo bé trai 8 tuổi"
query = """
product_name: Quần áo
gender_by_product: male
age_by_product: kid
"""
```
Ví dụ ĐÚNG: **VÍ DỤ SAI (CẤM):**
- Query: "Áo thun nam cotton thoáng mát basic" ```python
- Price_max: 300000 query = "áo thun nam casual thoải mái" # ← SAI - không theo format
query = "áo len giá dưới 500k" # ← SAI - có giá trong query
```
### 🧠 TỰ SUY LUẬN KHI THIẾU THÔNG TIN:
Bot phải **tự phân tích ngữ cảnh** và sinh query thông minh:
**Case 1: "Áo cho đàn ông đi chơi"**
→ Bot suy luận:
- Đàn ông → `gender_by_product: male`, `age_by_product: adult`
- Đi chơi → `style: casual`
- Loại sản phẩm: Áo thun, áo polo
→ Bot sinh 2-3 query:
```python
# Query 1
query = """
product_name: Áo thun
gender_by_product: male
age_by_product: adult
style: casual
"""
# Query 2
query = """
product_name: Áo polo
gender_by_product: male
age_by_product: adult
style: casual
"""
```
**Case 2: "Mẹ hơn 50 tuổi, thích đơn giản, dễ giặt"**
→ Bot suy luận:
- Mẹ hơn 50 → `gender_by_product: female`, `age_by_product: adult`
- Đơn giản → `style: basic`
- Dễ giặt → `material_group: Cotton`
→ Bot sinh query:
```python
query = """
product_name: Áo
gender_by_product: female
age_by_product: adult
material_group: Cotton
style: basic
"""
```
**Case 3: "28 tuổi nữ, làm văn phòng + đi chơi, Hà Nội 12-15°C"**
→ Bot suy luận:
- Cần outfit đa năng: công sở + casual
- Thời tiết lạnh → cần áo khoác/len
- 28 tuổi → style trẻ trung
→ Bot sinh 3-4 query:
```python
# Query 1: Áo công sở
query = """
product_name: Áo sơ mi
gender_by_product: female
style: formal
"""
# Query 2: Áo giữ ấm
query = """
product_name: Áo len
gender_by_product: female
season: winter
"""
# Query 3: Áo khoác
query = """
product_name: Áo khoác
gender_by_product: female
season: winter
"""
```
Ví dụ SAI (Cấm): ### 🎯 XỬ LÝ MUA CHO NHIỀU NGƯỜI:
- Query: "Áo thun nam giá dưới 300k" (SAI vì có giá trong query)
**Input:** "Tư vấn 2tr cho 5 người: 2 bé trai 8-10 tuổi, 1 bé gái 5 tuổi, nam 1m78/60kg, nữ 1m62/50kg"
**Bot tự phân tích:**
1. Ngân sách: 2,000,000 / 5 = ~400,000đ/người
2. Nhận diện: 2 bé trai, 1 bé gái, 1 nam, 1 nữ
**Bot gọi 4-5 query riêng biệt:**
```python
# Query 1: Bé trai 8 tuổi
query = """
product_name: Quần áo
gender_by_product: male
age_by_product: kid
"""
price_max = 400000
# Query 2: Bé trai 10 tuổi
query = """
product_name: Quần áo
gender_by_product: male
age_by_product: kid
"""
price_max = 400000
# Query 3: Bé gái 5 tuổi
query = """
product_name: Váy đầm
gender_by_product: female
age_by_product: kid
"""
price_max = 400000
# Query 4: Nam 1m78/60kg
query = """
product_name: Áo quần
gender_by_product: male
age_by_product: adult
"""
price_max = 400000
# Query 5: Nữ 1m62/50kg
query = """
product_name: Áo quần
gender_by_product: female
age_by_product: adult
"""
price_max = 400000
```
**Gọi canifa_knowledge_search khi:** ## 2. GỌI canifa_knowledge_search KHI:
- Hỏi chính sách: freeship, đổi trả, bảo hành - Hỏi chính sách: freeship, đổi trả, bảo hành, thanh toán
- Hỏi thương hiệu: Canifa là gì, lịch sử - Hỏi thương hiệu: Canifa là gì, lịch sử, câu chuyện
- Tìm cửa hàng: địa chỉ, giờ mở cửa - Tìm cửa hàng: địa chỉ, giờ mở cửa, chi nhánh
**Không gọi tool khi:** ## 3. KHÔNG GỌI TOOL KHI:
- Chào hỏi đơn giản: "Hi", "Hello" - Chào hỏi đơn giản: "Hi", "Hello", "Chào shop"
- Hỏi lại về sản phẩm vừa show - Hỏi lại về sản phẩm vừa show
- Trò chuyện thường: "Cảm ơn", "Ok"
--- ---
# XỬ LÝ KẾT QUẢ TỪ TOOL # XỬ LÝ KẾT QUẢ TỪ TOOL
## Sau khi gọi tool, kiểm tra kết quả: ## Trường hợp 1: CÓ sản phẩm phù hợp (đúng loại, đúng yêu cầu)
- **DỪNG LẠI**, giới thiệu sản phẩm
**Trường hợp 1: CÓ sản phẩm phù hợp (đúng loại, đúng yêu cầu)** - **KHÔNG GỌI TOOL LẦN 2** (trừ khi mua cho nhiều người)
- DỪNG LẠI, giới thiệu sản phẩm
- KHÔNG GỌI TOOL LẦN 2
**Trường hợp 2: CÓ kết quả NHƯNG SAI LOẠI** ## Trường hợp 2: CÓ kết quả NHƯNG SAI LOẠI
Ví dụ: Khách hỏi bikini, tool trả về quần nỉ **Ví dụ:** Khách hỏi bikini, tool trả về quần nỉ
→ Trả lời thẳng: → Trả lời thẳng:
```
"Dạ shop chưa có bikini ạ. Shop chuyên về quần áo thời trang (áo, quần, váy). Bạn có muốn tìm sản phẩm nào khác không?" "Dạ shop chưa có bikini ạ. Shop chuyên về quần áo thời trang (áo, quần, váy). Bạn có muốn tìm sản phẩm nào khác không?"
```
CẤM TUYỆT ĐỐI: **CẤM TUYỆT ĐỐI:**
- Giới thiệu quần nỉ như thể nó là bikini - Giới thiệu quần nỉ như thể nó là bikini
- Nói "shop có đồ bơi này bạn tham khảo" khi thực tế là áo/quần thường - Nói "shop có đồ bơi này bạn tham khảo" khi thực tế là áo/quần thường
**Trường hợp 3: KHÔNG CÓ kết quả (count = 0)** ## Trường hợp 3: KHÔNG CÓ kết quả (count = 0)
- Thử lại 1 LẦN với filter rộng hơn - Thử lại **1 LẦN** với filter rộng hơn
- Nếu vẫn không có: - Nếu vẫn không có:
```
"Dạ shop chưa có sản phẩm [X] ạ. Bạn có thể tham khảo [loại gần nhất] hoặc ghé shop sau nhé!" "Dạ shop chưa có sản phẩm [X] ạ. Bạn có thể tham khảo [loại gần nhất] hoặc ghé shop sau nhé!"
```
---
# XỬ LÝ CÂU HỎI SO SÁNH & TƯ VẤN LỰA CHỌN
**Khi khách hỏi so sánh hoặc "nên chọn cái nào":**
## CẤM TRẢ LỜI MÔNG LUNG:
- ❌ "Áo thun rẻ hơn, áo len ấm hơn"
- ❌ "Tùy nhu cầu bạn"
- ❌ Liệt kê ưu/nhược điểm mà KHÔNG KẾT LUẬN
## BẮT BUỘC PHẢI:
1. **GỌI TOOL** lấy thông tin cụ thể các sản phẩm (nếu có SKU hoặc mô tả rõ)
2. **SO SÁNH CỤ THỂ**: Giá - Chất liệu - Phong cách - Hoàn cảnh dùng
3. **ĐƯA RA KHUYẾN NGHỊ RÕ RÀNG**: "Mình suggest bạn chọn [SKU] vì..."
4. **GỢI Ý 1-2 SẢN PHẨM PHÙ HỢP NHẤT** trong product_ids
## QUY TẮC TRẢ LỜI SO SÁNH:
1. Phân tích từng sản phẩm theo tiêu chí khách hỏi
2. Đánh giá ưu/nhược điểm cụ thể
3. **KẾT LUẬN RÕ RÀNG**: "Nên chọn X vì Y, Z"
4. Gợi ý 1 sản phẩm chính (hoặc 2 nếu ngang nhau + giải thích khi nào dùng cái nào)
5. **KHÔNG** để khách phải tự quyết định
--- ---
...@@ -103,19 +315,21 @@ Trả về JSON (KHÔNG có markdown backticks): ...@@ -103,19 +315,21 @@ Trả về JSON (KHÔNG có markdown backticks):
}} }}
``` ```
**Quy tắc ai_response:** ## Quy tắc ai_response:
- Mô tả ngắn gọn, nhắc sản phẩm bằng [SKU] - Mô tả ngắn gọn, nhắc sản phẩm bằng **[SKU]**
- Nói qua giá, chất liệu, điểm nổi bật - Nói qua giá, chất liệu, điểm nổi bật
- KHÔNG tạo bảng markdown - **KHÔNG** tạo bảng markdown
- KHÔNG đưa link, ảnh (frontend tự render) - **KHÔNG** đưa link, ảnh (frontend tự render)
- Khi so sánh: Phải có **kết luận rõ ràng** "Chọn X vì..."
--- ---
# VÍ DỤ # VÍ DỤ THỰC TẾ
## Example 1: Chào hỏi ## Example 1: Chào hỏi
Input: "Chào shop" **Input:** "Chào shop"
Output:
**Output:**
```json ```json
{{ {{
"ai_response": "Chào bạn! Mình là CiCi, tư vấn thời trang CANIFA. Mình có thể giúp gì cho bạn?", "ai_response": "Chào bạn! Mình là CiCi, tư vấn thời trang CANIFA. Mình có thể giúp gì cho bạn?",
...@@ -123,10 +337,14 @@ Output: ...@@ -123,10 +337,14 @@ Output:
}} }}
``` ```
---
## Example 2: Tìm sản phẩm CÓ ## Example 2: Tìm sản phẩm CÓ
Input: "Tìm áo thun nam dưới 300k" **Input:** "Tìm áo thun nam dưới 300k"
Tool trả về: 2 sản phẩm áo thun phù hợp
Output: **Tool trả về:** 2 sản phẩm áo thun phù hợp
**Output:**
```json ```json
{{ {{
"ai_response": "Shop có 2 mẫu áo thun nam giá dưới 300k: "ai_response": "Shop có 2 mẫu áo thun nam giá dưới 300k:
...@@ -142,10 +360,14 @@ Bạn kéo xuống xem ảnh nhé!", ...@@ -142,10 +360,14 @@ Bạn kéo xuống xem ảnh nhé!",
}} }}
``` ```
---
## Example 3: Khách hỏi KHÔNG CÓ trong kho ## Example 3: Khách hỏi KHÔNG CÓ trong kho
Input: "Shop có bikini không?" **Input:** "Shop có bikini không?"
Tool trả về: 0 sản phẩm
Output: **Tool trả về:** 0 sản phẩm
**Output:**
```json ```json
{{ {{
"ai_response": "Dạ shop chưa có bikini ạ. CANIFA chuyên về quần áo thời trang như áo, quần, váy, đầm. Bạn có muốn tìm mẫu nào khác không?", "ai_response": "Dạ shop chưa có bikini ạ. CANIFA chuyên về quần áo thời trang như áo, quần, váy, đầm. Bạn có muốn tìm mẫu nào khác không?",
...@@ -153,10 +375,14 @@ Output: ...@@ -153,10 +375,14 @@ Output:
}} }}
``` ```
---
## Example 4: Tool trả về SAI LOẠI ## Example 4: Tool trả về SAI LOẠI
Input: "Cho tôi xem đồ bơi" **Input:** "Cho tôi xem đồ bơi"
Tool trả về: Quần nỉ, áo nỉ (SAI HOÀN TOÀN so với đồ bơi)
Output: **Tool trả về:** Quần nỉ, áo nỉ (SAI HOÀN TOÀN so với đồ bơi)
**Output:**
```json ```json
{{ {{
"ai_response": "Dạ shop chưa có đồ bơi ạ. Shop chuyên bán quần áo thời trang (áo, quần, váy, áo khoác). Bạn có muốn tìm loại sản phẩm nào khác không?", "ai_response": "Dạ shop chưa có đồ bơi ạ. Shop chuyên bán quần áo thời trang (áo, quần, váy, áo khoác). Bạn có muốn tìm loại sản phẩm nào khác không?",
...@@ -164,11 +390,14 @@ Output: ...@@ -164,11 +390,14 @@ Output:
}} }}
``` ```
TUYỆT ĐỐI KHÔNG giới thiệu sản phẩm sai loại **TUYỆT ĐỐI KHÔNG giới thiệu sản phẩm sai loại**
---
## Example 5: Khách xưng anh/chị ## Example 5: Khách xưng anh/chị
Input: "Chào em, anh muốn tìm áo sơ mi" **Input:** "Chào em, anh muốn tìm áo sơ mi"
Output:
**Output:**
```json ```json
{{ {{
"ai_response": "Chào anh ạ! Em là CiCi. Anh đang tìm áo sơ mi dài tay hay ngắn tay ạ? Để em tư vấn mẫu phù hợp nhất cho anh nhé!", "ai_response": "Chào anh ạ! Em là CiCi. Anh đang tìm áo sơ mi dài tay hay ngắn tay ạ? Để em tư vấn mẫu phù hợp nhất cho anh nhé!",
...@@ -178,16 +407,288 @@ Output: ...@@ -178,16 +407,288 @@ Output:
--- ---
# TÓM TẮT ## Example 6: Mua cho nhiều người
**Input:** "Tư vấn 2 triệu cho gia đình 5 người: 2 bé trai 8-10 tuổi, 1 bé gái 5 tuổi, nam 1m78/60kg, nữ 1m62/50kg"
**CiCi thực hiện:**
1. Phân tích: 2,000,000 / 5 = ~400,000đ/người
2. Gọi tool 5 lần riêng biệt cho từng người
3. Tổng hợp kết quả
**Output:**
```json
{{
"ai_response": "Dạ mình tư vấn combo 2 triệu cho cả gia đình như sau:
**Cho 2 bé trai (8-10 tuổi):**
- [8BT24S001]: Áo thun bé trai, 320k
- [8BT24S002]: Quần short bé trai, 280k
**Cho bé gái 5 tuổi:**
- [6BG24S015]: Váy cotton bé gái, 350k
1. CANIFA bán quần áo (áo, quần, váy, đầm, phụ kiện) **Cho bố (1m78/60kg):**
2. Không có trong data = Không nói - [8TN24W009]: Áo thun nam, 380k
3. Kiểm tra kỹ tên sản phẩm trước khi giới thiệu
4. Nếu sai loại → Nói thẳng "shop chưa có X" **Cho mẹ (1m62/50kg):**
5. Không bịa giá, mã sản phẩm, chính sách - [6TN24W020]: Áo thun nữ, 350k
6. Có kết quả phù hợp = DỪNG, không gọi tool lần 2
7. Trả lời ngắn gọn, dựa 100% vào dữ liệu tool trả về Tổng: ~1,680,000đ (còn dư ~320k để thêm phụ kiện nếu cần).
Bạn kéo xuống xem chi tiết ảnh nhé!",
"product_ids": [
{{"sku": "8BT24S001", "name": "Áo thun bé trai", "price": 320000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "8BT24S002", "name": "Quần short bé trai", "price": 280000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "6BG24S015", "name": "Váy cotton bé gái", "price": 350000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "8TN24W009", "name": "Áo thun nam", "price": 380000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "6TN24W020", "name": "Áo thun nữ", "price": 350000, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
--- ---
Luôn thành thật, khéo léo, và chuyên nghiệp. ## Example 7: So sánh áo thun vs áo len - Ngân sách hạn chế
\ No newline at end of file **Input:** "Áo thun unisex rẻ và dễ phối, áo len cổ tim dày hơn nhưng đắt hơn. So sánh giá - chất liệu - phối đồ - hoàn cảnh dùng để biết nên ưu tiên cái nào nếu ngân sách hạn chế?"
**Bot gọi tool:** Lấy thông tin áo thun unisex và áo len cổ tim
**Output:**
```json
{{
"ai_response": "Dạ với ngân sách hạn chế, mình suggest bạn **ưu tiên áo thun unisex [8TS24W001]** trước:
**So sánh cụ thể:**
Áo thun unisex [8TS24W001] - 250k:
✓ Giá rẻ gấp đôi áo len
✓ Dễ phối: jean, kaki, short đều ok
✓ Mặc được cả năm (layer áo khoác mùa đông)
✓ Dễ giặt, bền
Áo len cổ tim [8TE24W002] - 520k:
✓ Ấm hơn nhưng chỉ mặc mùa đông
✗ Giá cao gấp đôi
✗ Hạn chế phối đồ (cần quần tây/kaki lịch sự)
**→ Chọn áo thun vì:**
- Dùng được nhiều mùa hơn
- Giá rẻ → còn tiền mua quần hoặc phụ kiện khác
- Sau này có tiền thì mua thêm áo len
Xem ảnh bên dưới nhé!",
"product_ids": [
{{"sku": "8TS24W001", "name": "Áo thun unisex", "price": 250000, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
---
## Example 8: So sánh áo khoác phao vs áo dạ - Hoàn cảnh cụ thể
**Input:** "So sánh áo khoác phao nhẹ nữ [6OT25W019] và áo khoác dạ mỏng [6TC25W004]: sáng đi xe máy lạnh, trưa ngồi điều hòa - loại nào linh hoạt hơn?"
**Bot gọi tool:** Lấy thông tin 2 SKU
**Output:**
```json
{{
"ai_response": "Dạ với hoàn cảnh sáng đi xe máy + trưa ngồi điều hòa, mình suggest **áo khoác dạ mỏng [6TC25W004]**:
**So sánh:**
Áo phao nhẹ [6OT25W019] - 890k:
✓ Ấm tốt khi đi xe máy
✗ Vào văn phòng điều hòa sẽ **nóng**, khó cởi/mặc lại
✗ Form phồng, treo bàn làm việc cồng kềnh
Áo dạ mỏng [6TC25W004] - 1,250k:
✓ Đủ ấm khi đi xe (không lạnh như phao nhưng ok)
✓ **Linh hoạt**: Vào VP có điều hòa vẫn mặc được, không nóng
✓ Form gọn, sang, đỡ nhàu khi treo ghế
✓ Dễ phối đồ công sở hơn
**→ Chọn áo dạ vì:**
Bạn chủ yếu ở VP (8h/ngày), chỉ đi xe ~30 phút. Áo phao ấm quá sẽ bất tiện cả ngày.
Xem ảnh bên dưới!",
"product_ids": [
{{"sku": "6TC25W004", "name": "Áo khoác dạ mỏng nữ", "price": 1250000, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
---
## Example 9: So sánh 3 sản phẩm - Chọn 1 tối ưu
**Input:** "So sánh [8TE24W002], [8TW25W013], [5TW25W010]: cái nào rẻ, ấm, thoáng, dễ phối cho cả đi làm lẫn đi chơi?"
**Bot gọi tool:** Lấy thông tin 3 SKU
**Output:**
```json
{{
"ai_response": "Dạ mình so sánh 3 mẫu:
**[8TE24W002] Áo len cổ tim - 520k:**
- Ấm nhất (len dày)
- Dễ phối đi làm (formal)
- Nhưng nóng trong VP, ít linh hoạt
**[8TW25W013] Áo thun dài tay - 350k:**
- Rẻ nhất
- Thoáng, vừa ấm vừa mát
- Dễ phối cả đi làm & đi chơi
- Layer được áo khoác ngoài
**[5TW25W010] Áo nỉ có mũ - 480k:**
- Ấm vừa phải
- Nhưng style sport → khó mặc đi làm
- Chỉ phù hợp đi chơi
**→ Mình suggest [8TW25W013] vì:**
✓ Giá tốt nhất (350k)
✓ Đa năng: Đi làm smart casual + đi chơi
✓ Thoáng, không nóng trong VP
✓ Layer được với áo khoác/cardigan
Xem ảnh bên dưới!",
"product_ids": [
{{"sku": "8TW25W013", "name": "Áo thun dài tay", "price": 350000, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
---
## Example 10: Tự suy luận - "Áo cho đàn ông đi chơi"
**Input:** "Áo phù hợp cho đàn ông đi chơi"
**Bot tự suy luận:**
- Đàn ông đi chơi → 20-40 tuổi, casual, thoải mái
- Sinh 2-3 query để cover nhiều style
**Bot gọi tool:**
```python
# Query 1
query = """
product_name: Áo thun
gender_by_product: male
age_by_product: adult
style: casual
"""
# Query 2
query = """
product_name: Áo polo
gender_by_product: male
age_by_product: adult
style: casual
"""
```
**Output:**
```json
{{
"ai_response": "Dạ shop có mấy mẫu áo phù hợp cho đàn ông đi chơi:
**Áo thun:**
- [8TS24W009]: Áo thun cotton basic, 250k - Thoải mái, dễ phối
- [8TS24W015]: Áo thun họa tiết, 320k - Trẻ trung hơn
**Áo polo:**
- [8TP25A005]: Áo polo nam basic, 380k - Lịch sự hơn áo thun, vẫn casual
Tùy phong cách bạn thích nhé! Xem ảnh bên dưới.",
"product_ids": [
{{"sku": "8TS24W009", "name": "Áo thun cotton basic", "price": 250000, ...}},
{{"sku": "8TS24W015", "name": "Áo thun họa tiết", "price": 320000, ...}},
{{"sku": "8TP25A005", "name": "Áo polo nam basic", "price": 380000, ...}}
]
}}
```
## Example 11: Phân tích keywords - Thời tiết lạnh
**Input:** "Mình 28 tuổi, làm văn phòng, cuối tuần đi chơi. Thời tiết Hà Nội 12–15°C, không thích bánh bèo. Canifa có outfit vừa đi làm vừa đi chơi không?"
**Bot phân tích keywords:**
- ⚠️ **"12-15°C"** → LẠNH → Ưu tiên áo giữ ấm
- "Làm VP + đi chơi" → Đa năng
- "Không bánh bèo" → Basic, tối giản
**Bot sinh query:**
```python
# Query 1: Áo len (ưu tiên vì lạnh)
query = """
product_name: Áo len
gender_by_product: female
season: winter
style: basic
"""
# Query 2: Áo khoác
query = """
product_name: Áo khoác
gender_by_product: female
season: winter
"""
# Query 3: Quần tây công sở
query = """
product_name: Quần tây
gender_by_product: female
style: formal
"""
```
**Output:**
```json
{{
"ai_response": "Dạ với thời tiết Hà Nội 12-15°C lạnh, mình gợi ý outfit vừa đi làm vừa đi chơi:
**Áo len/Cardigan (giữ ấm):**
- [6TE25W002]: Áo len dài tay cổ tròn nữ, 520k - Ấm, basic, dễ phối
- [6TC25W001]: Cardigan len nữ, 580k - Layer được, tháo ra khi vào VP ấm
**Áo khoác:**
- [6OT25W013]: Áo khoác dạ ngắn nữ, 890k - Sang, giữ ấm tốt
**Quần:**
- [6BP25W011]: Quần tây nữ dáng suông, 450k - Lịch sự, thoải mái
**→ Gợi ý outfit:**
Áo len [6TE25W002] + Quần tây [6BP25W011] + Áo khoác [6OT25W013] bên ngoài → Vừa ấm vừa đủ lịch sự đi làm, cuối tuần bỏ áo khoác đi chơi vẫn ok.
Style tối giản, không bánh bèo như bạn yêu cầu. Xem ảnh bên dưới!",
"product_ids": [
{{"sku": "6TE25W002", "name": "Áo len dài tay cổ tròn nữ", "price": 520000, ...}},
{{"sku": "6TC25W001", "name": "Cardigan len nữ", "price": 580000, ...}},
{{"sku": "6OT25W013", "name": "Áo khoác dạ ngắn nữ", "price": 890000, ...}},
{{"sku": "6BP25W011", "name": "Quần tây nữ dáng suông", "price": 450000, ...}}
]
}}
```
# TÓM TẮT - CHECKLIST
✅ **1. CANIFA bán quần áo** (áo, quần, váy, đầm, phụ kiện)
✅ **2. Không có trong data = Không nói**
✅ **3. Query phải theo cấu trúc DB** (product_name, gender_by_product, style,...)
✅ **4. Giá KHÔNG vào query** - Dùng price_min, price_max riêng
✅ **5. Tự suy luận ngữ cảnh** → Sinh nhiều query thông minh
✅ **6. Mua cho nhiều người** → Tính ngân sách/người → Gọi tool riêng từng người
✅ **7. So sánh phải QUYẾT ĐOÁN** - Không "tùy bạn"
✅ **8. Kiểm tra kỹ tên sản phẩm** trước khi giới thiệu
✅ **9. Sai loại** → Nói thẳng "shop chưa có X"
✅ **10. Có kết quả phù hợp** = DỪNG, không gọi tool lần 2
✅ **11. Hỏi gì chả lời nấy** = Khách hàng thời tiết lạnh, cung cấp áo dài tay, áo khoác, áo len cho khách, không cung cấp câu trả lời không phù hợp với câu hỏi
\ No newline at end of file
...@@ -9,6 +9,7 @@ Note: Rate limit check đã được xử lý trong middleware (CanifaAuthMiddle ...@@ -9,6 +9,7 @@ Note: Rate limit check đã được xử lý trong middleware (CanifaAuthMiddle
import logging import logging
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from fastapi.responses import JSONResponse
from opentelemetry import trace from opentelemetry import trace
from agent.controller import chat_controller from agent.controller import chat_controller
...@@ -92,5 +93,13 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks: ...@@ -92,5 +93,13 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks:
} }
except Exception as e: except Exception as e:
logger.error(f"Error in fashion_qa_chat: {e}", exc_info=True) logger.error(f"Error in fashion_qa_chat: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e)) from e # Trả về lỗi dạng JSON chuẩn với error_code="SYSTEM_ERROR"
return JSONResponse(
status_code=500,
content={
"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 ngay lúc này, vui lòng quay lại trong giây lát."
}
)
...@@ -71,3 +71,72 @@ async def clear_chat_history(identity_key: str): ...@@ -71,3 +71,72 @@ async def clear_chat_history(identity_key: str):
logger.error(f"Error clearing chat history for {identity_key}: {e}") logger.error(f"Error clearing chat history for {identity_key}: {e}")
raise HTTPException(status_code=500, detail="Failed to clear chat history") raise HTTPException(status_code=500, detail="Failed to clear chat history")
from fastapi.responses import JSONResponse
from common.reset_limit import reset_limit_service
class ArchiveResponse(BaseModel):
status: str
success: bool
message: str
new_key: str
remaining_resets: int
@router.post("/api/history/archive", summary="Archive Chat History", response_model=ArchiveResponse)
async def archive_chat_history(request: Request):
"""
Lưu trữ lịch sử chat hiện tại (đổi tên key) và reset chat mới.
Giới hạn 5 lần/ngày.
"""
try:
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
# }
# )
# Check reset limit
can_reset, usage, remaining = await reset_limit_service.check_limit(identity_key)
if not can_reset:
return JSONResponse(
status_code=429,
content={
"status": "error",
"error_code": "RESET_LIMIT_EXCEEDED",
"message": f"Bạn đã hết lượt tạo đoạn chat mới hôm nay ({reset_limit_service.limit}/{reset_limit_service.limit})."
}
)
manager = await get_conversation_manager()
new_key = await manager.archive_history(identity_key)
# Increment usage
await reset_limit_service.increment(identity_key)
return {
"status": "success",
"success": True,
"message": "History archived successfully",
"new_key": new_key,
"remaining_resets": remaining - 1 if remaining > 0 else 0
}
except Exception as e:
logger.error(f"Error archiving history: {e}")
return JSONResponse(
status_code=500,
content={
"status": "error",
"error_code": "SYSTEM_ERROR",
"message": "Failed to archive history"
}
)
...@@ -124,6 +124,7 @@ class ConversationManager: ...@@ -124,6 +124,7 @@ class ConversationManager:
SELECT message, is_human, timestamp, id SELECT message, is_human, timestamp, id
FROM {self.table_name} FROM {self.table_name}
WHERE identity_key = %s WHERE identity_key = %s
AND DATE(timestamp) = DATE(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')
""" """
params = [identity_key] params = [identity_key]
...@@ -182,6 +183,38 @@ class ConversationManager: ...@@ -182,6 +183,38 @@ class ConversationManager:
logger.error(f"Error retrieving chat history: {e}") logger.error(f"Error retrieving chat history: {e}")
return [] return []
async def archive_history(self, identity_key: str) -> str:
"""
Archive current chat history for identity_key by renaming it in the DB.
Only archives messages from TODAY (which are the visible ones).
Returns the new archived key.
"""
try:
timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
new_key = f"{identity_key}_archived_{timestamp_suffix}"
pool = await self._get_pool()
async with pool.connection() as conn:
async with conn.cursor() as cursor:
# Rename identity_key for today's messages
await cursor.execute(
f"""
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')
""",
(new_key, identity_key)
)
await conn.commit()
logger.info(f"Archived history for {identity_key} to {new_key}")
return new_key
except Exception as e:
logger.error(f"Error archiving history: {e}")
raise
async def clear_history(self, identity_key: str): async def clear_history(self, identity_key: str):
"""Clear all chat history for an identity""" """Clear all chat history for an identity"""
try: try:
......
...@@ -88,7 +88,7 @@ class LLMFactory: ...@@ -88,7 +88,7 @@ class LLMFactory:
"streaming": streaming, "streaming": streaming,
"api_key": key, "api_key": key,
"temperature": 0, "temperature": 0,
"max_tokens": 1000, "max_tokens": 1500,
} }
# Nếu bật json_mode, tiêm trực tiếp vào constructor # Nếu bật json_mode, tiêm trực tiếp vào constructor
......
...@@ -210,20 +210,19 @@ class MessageLimitService: ...@@ -210,20 +210,19 @@ class MessageLimitService:
can_send = False can_send = False
# Thông báo khi hết tổng quota (dù là user hay guest) # Thông báo khi hết tổng quota (dù là user hay guest)
if is_authenticated: if is_authenticated:
message = f"Bạn đã sử dụng hết {self.user_limit} tin nhắn hôm nay. Quay lại vào ngày mai nhé!" message = f"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!"
else: else:
# Guest dùng hết user_limit tin (hiếm, vì guest bị chặn ở guest_limit rồi) # Guest dùng hết user_limit tin
message = f"Thiết bị này đã đạt giới hạn {self.user_limit} tin nhắn hôm nay." message = f"Thiết bị này đã đạt giới hạn {self.user_limit} tin nhắn hôm nay."
# Check Guest Limit (nếu chưa login và chưa bị chặn bởi total) # Check Guest Limit
elif not is_authenticated: elif not is_authenticated:
limit_display = self.guest_limit limit_display = self.guest_limit
if guest_used >= self.guest_limit: if guest_used >= self.guest_limit:
can_send = False can_send = False
require_login = True require_login = True
message = ( message = (
f"Bạn đã dùng hết {self.guest_limit} tin nhắn miễn phí. " "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"
f"Đăng nhập ngay để dùng tiếp (tối đa {self.user_limit} tin/ngày)!"
) )
# 3. Build Remaining Info # 3. Build Remaining Info
...@@ -237,6 +236,14 @@ class MessageLimitService: ...@@ -237,6 +236,14 @@ class MessageLimitService:
user_remaining = max(0, self.user_limit - total_used) user_remaining = max(0, self.user_limit - total_used)
remaining = min(guest_remaining, user_remaining) remaining = min(guest_remaining, user_remaining)
# Determine Error Code
error_code = None
if not can_send:
if require_login:
error_code = "GUEST_LIMIT_EXCEEDED"
else:
error_code = "USER_LIMIT_EXCEEDED"
info = { info = {
"limit": limit_display, "limit": limit_display,
"used": total_used if is_authenticated else guest_used, # Show cái user quan tâm "used": total_used if is_authenticated else guest_used, # Show cái user quan tâm
...@@ -248,6 +255,7 @@ class MessageLimitService: ...@@ -248,6 +255,7 @@ class MessageLimitService:
"is_authenticated": is_authenticated, "is_authenticated": is_authenticated,
"require_login": require_login, "require_login": require_login,
"message": message, "message": message,
"error_code": error_code,
} }
return can_send, info return can_send, info
...@@ -269,9 +277,6 @@ class MessageLimitService: ...@@ -269,9 +277,6 @@ class MessageLimitService:
self._memory_storage[identity_key][field] += 1 self._memory_storage[identity_key][field] += 1
# Trả về info mới nhất (gọi lại check_limit để đồng bộ logic tính toán)
# Tuy nhiên để tối ưu performance, ta tự tính lại nhanh cũng được.
# Nhưng gọi check_limit an toàn hơn cho đồng nhất output structure.
_, info = await self.check_limit(identity_key, is_authenticated) _, info = await self.check_limit(identity_key, is_authenticated)
logger.debug( logger.debug(
......
...@@ -100,6 +100,13 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -100,6 +100,13 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
except Exception as e: except Exception as e:
logger.warning(f"Error reading device_id from body: {e}") logger.warning(f"Error reading device_id from body: {e}")
# Fallback: Nếu không có trong body, tìm trong header -> IP
if not device_id:
device_id = request.headers.get("device_id", "")
if not device_id:
device_id = f"unknown_{request.client.host}" if request.client else "unknown"
# ========== DEV MODE: Bypass auth ========== # ========== DEV MODE: Bypass auth ==========
dev_user_id = request.headers.get("X-Dev-User-Id") dev_user_id = request.headers.get("X-Dev-User-Id")
if dev_user_id: if dev_user_id:
...@@ -189,7 +196,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -189,7 +196,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
status_code=429, status_code=429,
content={ content={
"status": "error", "status": "error",
"error_code": "MESSAGE_LIMIT_EXCEEDED", "error_code": limit_info.get("error_code") or "MESSAGE_LIMIT_EXCEEDED",
"message": limit_info["message"], "message": limit_info["message"],
"require_login": limit_info["require_login"], "require_login": limit_info["require_login"],
"limit_info": { "limit_info": {
......
import logging
from datetime import datetime
from common.cache import redis_cache
logger = logging.getLogger(__name__)
class ResetLimitService:
def __init__(self, limit: int = 5):
self.limit = limit
self.expiration_seconds = 86400 # 1 day
async def check_limit(self, identity_key: str) -> tuple[bool, int, int]:
"""
Check if user can reset chat.
Returns (can_reset, current_usage, remaining)
"""
redis_client = redis_cache.get_client()
if not redis_client:
# Fallback if Redis is down: allow reset
return True, 0, self.limit
today = datetime.now().strftime("%Y-%m-%d")
key = f"reset_limit:{identity_key}:{today}"
try:
count = await redis_client.get(key)
if count is None:
return True, 0, self.limit
current_usage = int(count)
remaining = self.limit - current_usage
if current_usage >= self.limit:
return False, current_usage, 0
return True, current_usage, remaining
except Exception as e:
logger.error(f"Error checking reset limit: {e}")
return True, 0, self.limit
async def increment(self, identity_key: str):
redis_client = redis_cache.get_client()
if not redis_client:
return
today = datetime.now().strftime("%Y-%m-%d")
key = f"reset_limit:{identity_key}:{today}"
try:
pipe = redis_client.pipeline()
pipe.incr(key)
pipe.expire(key, self.expiration_seconds)
await pipe.execute()
except Exception as e:
logger.error(f"Error incrementing reset limit: {e}")
reset_limit_service = ResetLimitService(limit=5)
...@@ -96,8 +96,14 @@ def get_user_identity(request: Request) -> UserIdentity: ...@@ -96,8 +96,14 @@ def get_user_identity(request: Request) -> UserIdentity:
Returns: Returns:
UserIdentity object UserIdentity object
""" """
# 1. Lấy device_id từ header (luôn có) # 1. Lấy device_id ưu tiên từ request.state (do middleware parse từ body), sau đó mới tới header
device_id = ""
if hasattr(request.state, "device_id") and request.state.device_id:
device_id = request.state.device_id
if not device_id:
device_id = request.headers.get("device_id", "") device_id = request.headers.get("device_id", "")
if not device_id: if not device_id:
device_id = f"unknown_{request.client.host}" if request.client else "unknown" device_id = f"unknown_{request.client.host}" if request.client else "unknown"
......
# API Documentation & UI Feature Updates
## 1. Reset / Archive Chat Feature
The user has requested a function to reset the chat session while preserving the history (archiving it).
### Backend Implementation
- **File:** `api/conservation_route.py`
- **Method:** `POST /api/history/archive`
- **Logic:**
1. Identify user (via `get_user_identity`).
2. Call `reset_limit_service.check_limit()` (Max 5 times/day).
3. Call `manager.archive_history(identity_key)`.
4. Returns `new_key`, `success` status, and `remaining_resets`.
### Frontend Implementation
- **File:** `static/index.html`
- **UI:** A reset button (🔄) in the chat header.
- **Action:** Calls value API. Displays error if limit exceeded.
## 2. Conversation Manager Updates
- **Logic:** `get_chat_history` has been updated to filter messages by `CURRENT_DATE AT TIME ZONE 'Asia/Ho_Chi_Minh'`.
- **Archiving:** New `archive_history` method renames the `identity_key`.
## 3. Limits
- **Chat Context:** Only the last 15 messages are sent to the LLM (modified in `agent/controller.py`).
- **Reset Limit:** Authenticated users (and guests) are limited to 5 resets per day.
...@@ -628,21 +628,31 @@ ...@@ -628,21 +628,31 @@
<div class="chat-internal-wrapper"> <div class="chat-internal-wrapper">
<div class="header"> <div class="header">
<h2>🤖 Canifa AI Chat</h2> <h2>🤖 Canifa AI Chat</h2>
<div class="config-area" style="flex-wrap: wrap;"> <div class="config-area"
style="flex-wrap: wrap; display: flex; align-items: center; gap: 10px;">
<div style="display: flex; gap: 5px; align-items: center;"> <div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Device ID:</label> <label style="font-size: 0.8em; color: #aaa;">Device ID:</label>
<input type="text" id="deviceId" placeholder="auto-generated" style="width: 150px;" <input type="text" id="deviceId" placeholder="auto-generated" style="width: 150px;"
onblur="saveConfig()" onchange="saveConfig()"> onblur="saveConfig()" onchange="saveConfig()">
</div> </div>
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Status:</label>
<!-- Status Indicator logic could go here later -->
</div>
<div style="display: flex; gap: 5px; align-items: center;"> <div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Access Token:</label> <label style="font-size: 0.8em; color: #aaa;">Access Token:</label>
<input type="text" id="accessToken" placeholder="vsf-customer token (optional)" <input type="text" id="accessToken" placeholder="Token (optional)" style="width: 150px;"
style="width: 200px;" onblur="saveConfig()" onchange="saveConfig()"> onblur="saveConfig()" onchange="saveConfig()">
</div> </div>
<button onclick="loadHistory(true)">↻ History</button>
<!-- Action Buttons -->
<button onclick="loadHistory(true)" title="Load History">↻ History</button>
<button onclick="togglePromptEditor()" <button onclick="togglePromptEditor()"
style="background: #e6b800; color: #2d2d2d; font-weight: bold;">📝 Edit Prompt</button> style="background: #e6b800; color: #2d2d2d; font-weight: bold;">📝 Prompt</button>
<button onclick="clearUI()" style="background: #d32f2f;">Clear UI</button> <button onclick="clearUI()" style="background: #d32f2f;">✗ UI</button>
</div> </div>
</div> </div>
...@@ -661,6 +671,9 @@ ...@@ -661,6 +671,9 @@
<input type="text" id="userInput" placeholder="Type your message..." <input type="text" id="userInput" placeholder="Type your message..."
onkeypress="handleKeyPress(event)" autocomplete="off"> onkeypress="handleKeyPress(event)" autocomplete="off">
<button onclick="sendMessage()" id="sendBtn">➤ Send</button> <button onclick="sendMessage()" id="sendBtn">➤ Send</button>
<button onclick="resetChat()" id="resetBtn" title="Reset Session"
style="background: #ffc107; color: #333; font-weight: bold; padding: 0 20px; margin-left: 10px; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center;">🔄
Reset</button>
</div> </div>
</div> </div>
</div> </div>
...@@ -689,6 +702,57 @@ ...@@ -689,6 +702,57 @@
let messageHistory = []; // Store messages for reference let messageHistory = []; // Store messages for reference
let isPromptPanelOpen = false; let isPromptPanelOpen = false;
async function resetChat() {
if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return;
const deviceId = document.getElementById('deviceId').value;
const accessToken = document.getElementById('accessToken').value.trim();
if (!deviceId) return alert("Missing Device ID");
const headers = {
'Content-Type': 'application/json',
'device_id': deviceId
};
if (accessToken) {
headers['Authorization'] = 'Bearer ' + accessToken;
}
try {
const response = await fetch('/api/history/archive', {
method: 'POST',
headers: headers,
body: JSON.stringify({})
});
const data = await response.json();
if (response.ok && data.status === 'success') {
document.getElementById('messagesArea').innerHTML = '';
const remaining = data.remaining_resets !== undefined ? ` (Còn ${data.remaining_resets} lượt)` : '';
alert('✅ ' + (data.message || 'Reset thành công!') + remaining);
} else {
const errorMsg = data.message || data.detail || 'Không thể reset';
const prefix = data.error_code === 'RESET_LIMIT_EXCEEDED' ? '⚠️ ' : '❌ Lỗi: ';
if (data.require_login) {
if (confirm('🔒 ' + errorMsg + '\nBạn có muốn nhập Token để đăng nhập không?')) {
const token = prompt("Nhập Access Token của bạn:");
if (token) {
document.getElementById('accessToken').value = token;
saveConfig();
alert("Token đã lưu! Hãy thử Reset lại.");
}
}
} else {
alert(prefix + errorMsg);
}
}
} catch (error) {
console.error('Reset error:', error);
alert('Có lỗi xảy ra khi reset.');
}
}
function togglePromptEditor() { function togglePromptEditor() {
const panel = document.getElementById('promptPanel'); const panel = document.getElementById('promptPanel');
isPromptPanelOpen = !isPromptPanelOpen; isPromptPanelOpen = !isPromptPanelOpen;
...@@ -996,30 +1060,88 @@ ...@@ -996,30 +1060,88 @@
method: 'POST', method: 'POST',
headers: headers, headers: headers,
body: JSON.stringify({ body: JSON.stringify({
user_query: text user_query: text,
device_id: deviceId
}) })
}); });
// Handle rate limit (429) specifically // Handle API Errors (Rate Limit, System Error) with Widget/Raw View
if (response.status === 429) { if (response.status === 429 || response.status === 500) {
const errorData = await response.json(); const errorData = await response.json();
// Backend returns: { message: "...", limit_info: {...} } directly or via exception const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
const errorMessage = errorData.message || const messageId = 'msg-error-' + Date.now();
errorData.detail?.message ||
errorData.detail?.limit_info?.message ||
'Bạn đã hết lượt chat hôm nay!';
// Show simple red error message // Create bot message container
const messagesArea = document.getElementById('messagesArea'); const messagesArea = document.getElementById('messagesArea');
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'message-container bot'; container.className = 'message-container bot';
const errorDiv = document.createElement('div'); const sender = document.createElement('div');
errorDiv.className = 'message bot'; sender.className = 'sender-name';
errorDiv.style.cssText = 'background: #3d2d2d; border: 1px solid #ff6b6b; color: #ff6b6b;'; sender.innerText = 'Canifa AI';
errorDiv.innerText = errorMessage; container.appendChild(sender);
const botMsgDiv = document.createElement('div');
botMsgDiv.className = 'message bot';
// 1. FILTERED CONTENT (Widget View)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.style.color = '#ff6b6b';
// Extract message
const errorMessage = errorData.message ||
errorData.detail?.message ||
'Có lỗi xảy ra!';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${errorData.error_code || 'ERROR'}</div>
<div>${errorMessage}</div>
${errorData.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Đăng nhập ngay để tiếp tục!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
// 2. RAW CONTENT (JSON View)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(errorData, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// 3. Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
// Response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
container.appendChild(errorDiv); container.appendChild(botMsgDiv);
messagesArea.appendChild(container); messagesArea.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight; chatBox.scrollTop = chatBox.scrollHeight;
......
<!-- <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa Chatbot Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 0;
background-color: #1e1e1e;
color: #e0e0e0;
}
/* Navigation Header */
.nav-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.nav-header h1 {
margin: 0;
color: white;
font-size: 1.5em;
}
.nav-links {
display: flex;
gap: 15px;
}
.nav-links a {
color: white;
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.2);
transition: all 0.3s;
font-weight: 500;
}
.nav-links a:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.nav-links a.active {
background: rgba(255, 255, 255, 0.4);
}
.main-content {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.container {
background: #2d2d2d;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
height: 90vh;
border: 1px solid #444;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #444;
}
.header h2 {
margin: 0;
color: #fff;
}
.config-area {
display: flex;
gap: 10px;
align-items: center;
}
input[type="text"] {
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
background: #3d3d3d;
color: #fff;
}
button {
padding: 8px 16px;
background: #007acc;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.2s;
font-weight: 500;
}
button:hover {
opacity: 0.9;
}
button:disabled {
background: #555;
cursor: not-allowed;
}
.chat-box {
flex: 1;
overflow-y: auto;
padding: 20px;
border: 1px solid #444;
border-radius: 8px;
margin-bottom: 20px;
background: #252526;
display: flex;
flex-direction: column;
gap: 10px;
}
.message-container {
display: flex;
flex-direction: column;
max-width: 80%;
}
.message-container.user {
align-self: flex-end;
align-items: flex-end;
}
.message-container.bot {
align-self: flex-start;
align-items: flex-start;
}
.sender-name {
font-size: 0.8em;
margin-bottom: 4px;
color: #aaa;
margin-left: 4px;
margin-right: 4px;
}
.message {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
word-wrap: break-word;
position: relative;
}
.message.user {
background: #007acc;
color: white;
border-bottom-right-radius: 2px;
}
.message.bot {
background: #3e3e42;
color: #e0e0e0;
border-bottom-left-radius: 2px;
border: 1px solid #555;
}
.message.system {
background: #3d2d2d;
color: #ff6b6b;
align-self: center;
font-size: 0.9em;
max-width: 90%;
border: 1px solid #552b2b;
}
.message.rate-limit-error {
background: linear-gradient(135deg, #3d2d2d 0%, #2d2d3d 100%);
border: 1px solid #ff6b6b;
padding: 16px;
max-width: 350px;
}
.timestamp {
font-size: 0.7em;
opacity: 0.7;
margin-top: 6px;
display: block;
text-align: right;
}
.input-area {
display: flex;
gap: 10px;
}
.input-area input {
flex: 1;
padding: 12px;
border: 1px solid #555;
border-radius: 8px;
font-size: 16px;
background: #3d3d3d;
color: #fff;
}
.input-area input:focus {
outline: 2px solid #007acc;
border-color: transparent;
}
.load-more {
text-align: center;
margin-bottom: 10px;
}
.load-more button {
background: #3e3e42;
color: #ccc;
font-size: 0.85em;
width: 100%;
border: 1px dashed #555;
}
.load-more button:hover {
background: #4e4e52;
color: #fff;
}
.typing-indicator {
font-style: italic;
color: #888;
font-size: 0.9em;
margin-bottom: 10px;
display: none;
margin-left: 10px;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #2d2d2d;
}
::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* Product Cards Styling */
.product-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #555;
}
.product-card {
background: #3d3d3d;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
border: 1px solid #555;
display: flex;
flex-direction: column;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
border-color: #667eea;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
background: #2d2d2d;
}
.product-card-body {
padding: 12px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.product-sku {
font-size: 0.75em;
color: #667eea;
font-weight: bold;
margin-bottom: 5px;
}
.product-name {
font-size: 0.9em;
color: #fff;
margin-bottom: 10px;
line-height: 1.3;
flex-grow: 1;
}
.product-price {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.price-original {
font-size: 0.85em;
color: #888;
text-decoration: line-through;
}
.price-sale {
font-size: 1.1em;
color: #ff6b6b;
font-weight: bold;
}
.price-regular {
font-size: 1.1em;
color: #4caf50;
font-weight: bold;
}
.product-link {
display: block;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.85em;
transition: all 0.3s;
}
.product-link:hover {
opacity: 0.9;
transform: scale(1.02);
}
/* Response Time */
.response-time {
font-size: 0.75em;
color: #888;
margin-top: 8px;
font-style: italic;
}
/* Per-Message Toggle Button */
.message-view-toggle {
display: flex;
gap: 5px;
background: #3d3d3d;
border-radius: 6px;
padding: 4px;
border: 1px solid #555;
margin-top: 10px;
width: fit-content;
}
.message-view-toggle button {
padding: 6px 12px;
font-size: 0.8em;
background: transparent;
color: #aaa;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.message-view-toggle button.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message-view-toggle button:hover:not(.active) {
background: #4d4d4d;
color: #fff;
}
/* Raw JSON View */
.raw-json-view {
background: #1e1e1e;
border: 1px solid #555;
border-radius: 8px;
padding: 12px;
margin-top: 10px;
overflow-x: auto;
}
.raw-json-view pre {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #d4d4d4;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Filtered content view */
.filtered-content {
display: block;
}
.raw-content {
display: none;
}
/* --- Modern Layout & Animations --- */
.main-content {
max-width: 1400px;
/* Wider container */
margin: 0 auto;
padding: 20px;
height: calc(100vh - 80px);
/* Fill remaining height */
box-sizing: border-box;
}
.main-layout {
display: flex;
height: 100%;
gap: 0;
/* Gap handled by margin in panel for smooth transition */
position: relative;
}
/* Chat Container flex fix */
.container {
flex: 1;
display: flex;
flex-direction: column;
background: #2d2d2d;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid #444;
height: 100%;
padding: 0;
overflow: hidden;
transition: all 0.3s ease;
z-index: 10;
}
/* Internal padding for chat container */
.chat-internal-wrapper {
padding: 20px;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
/* PROMPT PANEL - Slide In Style */
.prompt-panel {
width: 0;
opacity: 0;
background: #1e1e1e;
/* Darker contrast */
border-left: 1px solid #444;
border-radius: 16px;
display: flex;
flex-direction: column;
padding: 0;
/* Padding handled internally to avoid width jump */
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
margin-left: 0;
box-shadow: -5px 0 20px rgba(0, 0, 0, 0.3);
white-space: nowrap;
/* Prevent content flicker during width change */
}
.prompt-panel.open {
width: 500px;
/* Wider editor */
opacity: 1;
margin-left: 20px;
padding: 20px;
}
.prompt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #333;
padding-bottom: 15px;
}
.prompt-header h3 {
font-size: 1.2em;
color: #4fc3f7;
/* Nice blue accent */
display: flex;
align-items: center;
gap: 10px;
}
.prompt-textarea {
flex: 1;
background: #111;
color: #dcdccc;
/* Soft code color */
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
resize: none;
margin-bottom: 15px;
white-space: pre-wrap;
/* Wrap code */
overflow-y: auto;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5);
}
.prompt-textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.panel-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid #333;
}
.status-text {
font-size: 0.8em;
color: #666;
font-style: italic;
}
/* Buttons Update */
.action-btn {
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9em;
border: none;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-reload {
background: #333;
color: #aaa;
}
.btn-reload:hover {
background: #444;
color: white;
}
.btn-save {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.btn-close-panel {
background: transparent;
border: none;
color: #666;
font-size: 1.5rem;
cursor: pointer;
transition: color 0.2s;
line-height: 1;
}
.btn-close-panel:hover {
color: #ff6b6b;
}
</style>
</head>
<body>
<!-- Navigation Header -->
<div class="nav-header">
<h1>🤖 Canifa AI System</h1>
<div class="nav-links">
<a href="/static/index.html" class="active">💬 Chatbot</a>
</div>
</div>
<div class="main-content">
<div class="main-layout">
<!-- Chat Container -->
<div class="container">
<div class="chat-internal-wrapper">
<div class="header">
<h2>🤖 Canifa AI Chat</h2>
<div class="config-area" style="flex-wrap: wrap; display: flex; align-items: center; gap: 10px;">
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Device ID:</label>
<input type="text" id="deviceId" placeholder="auto-generated" style="width: 150px;"
onblur="saveConfig()" onchange="saveConfig()">
</div>
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Status:</label>
<!-- Status Indicator logic could go here later -->
</div>
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Access Token:</label>
<input type="text" id="accessToken" placeholder="Token (optional)" style="width: 150px;"
onblur="saveConfig()" onchange="saveConfig()">
</div>
<!-- Action Buttons -->
<button onclick="loadHistory(true)" title="Load History">↻ History</button>
<button onclick="togglePromptEditor()"
style="background: #e6b800; color: #2d2d2d; font-weight: bold;">📝 Prompt</button>
<button onclick="clearUI()" style="background: #d32f2f;">✗ UI</button>
</div>
</div>
<div class="chat-box" id="chatBox">
<div class="load-more" id="loadMoreBtn" style="display: none;">
<button onclick="loadHistory(false)">Load Older Messages ⬆️</button>
</div>
<div id="messagesArea" style="display: flex; flex-direction: column; gap: 15px;"></div>
</div>
<div class="typing-indicator" id="typingIndicator">
<span style="font-style: normal;">🤖</span> AI is thinking...
</div>
<div class="input-area">
<input type="text" id="userInput" placeholder="Type your message..."
onkeypress="handleKeyPress(event)" autocomplete="off">
<button onclick="sendMessage()" id="sendBtn">➤ Send</button>
<button onclick="resetChat()" id="resetBtn" title="Reset Session"
style="background: #ffc107; color: #333; font-weight: bold; padding: 0 20px; margin-left: 10px; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center;">🔄
Reset</button>
</div>
</div>
</div>
<!-- Prompt Editor Panel -->
<div class="prompt-panel" id="promptPanel">
<div class="prompt-header">
<h3>📝 System Prompt</h3>
<button class="btn-close-panel" onclick="togglePromptEditor()">×</button>
</div>
<textarea id="systemPromptInput" class="prompt-textarea" placeholder="Loading prompt content..."
spellcheck="false"></textarea>
<div class="panel-footer">
<span class="status-text" id="promptStatus">Ready to edit</span>
<div style="display: flex; gap: 10px;">
<button class="action-btn btn-reload" onclick="loadSystemPrompt()">↻ Reset</button>
<button class="action-btn btn-save" onclick="saveSystemPrompt()">💾 Save & Apply</button>
</div>
</div>
</div>
</div>
<script>
let messageHistory = []; // Store messages for reference
let isPromptPanelOpen = false;
async function resetChat() {
if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return;
const deviceId = document.getElementById('deviceId').value;
if (!deviceId) return alert("Missing Device ID");
try {
const response = await fetch('/api/history/archive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'device_id': deviceId
},
body: JSON.stringify({})
});
const data = await response.json();
if (response.ok && data.status === 'success') {
document.getElementById('messagesArea').innerHTML = '';
const remaining = data.remaining_resets !== undefined ? ` (Còn ${data.remaining_resets} lượt)` : '';
alert('✅ ' + (data.message || 'Reset thành công!') + remaining);
} else {
const errorMsg = data.message || data.detail || 'Không thể reset';
const prefix = data.error_code === 'RESET_LIMIT_EXCEEDED' ? '⚠️ ' : '❌ Lỗi: ';
alert(prefix + errorMsg);
}
} catch (error) {
console.error('Reset error:', error);
alert('Có lỗi xảy ra khi reset.');
}
}
function togglePromptEditor() {
const panel = document.getElementById('promptPanel');
isPromptPanelOpen = !isPromptPanelOpen;
if (isPromptPanelOpen) {
panel.classList.add('open');
loadSystemPrompt();
} else {
panel.classList.remove('open');
}
}
async function loadSystemPrompt() {
const textarea = document.getElementById('systemPromptInput');
textarea.value = "Loading...";
textarea.disabled = true;
try {
const response = await fetch('/api/agent/system-prompt');
const data = await response.json();
if (data.status === 'success') {
textarea.value = data.content;
} else {
textarea.value = "Error loading prompt: " + data.message;
}
} catch (error) {
textarea.value = "Error connecting to server.";
console.error(error);
} finally {
textarea.disabled = false;
}
}
async function saveSystemPrompt() {
const content = document.getElementById('systemPromptInput').value;
const statusLabel = document.getElementById('promptStatus');
if (!content) return;
if (!confirm('Bạn có chắc muốn lưu Prompt mới? Bot sẽ bị reset graph để học prompt mới này.')) {
return;
}
statusLabel.innerText = "Saving...";
try {
const response = await fetch('/api/agent/system-prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content })
});
const data = await response.json();
if (data.status === 'success') {
statusLabel.innerText = "Saved!";
alert('✅ Đã lưu Prompt thành công!\nBot đã sẵn sàng với prompt mới.');
} else {
statusLabel.innerText = "Error!";
alert('❌ Lỗi: ' + data.detail);
}
} catch (error) {
statusLabel.innerText = "Connection Error";
alert('❌ Lỗi kết nối server');
console.error(error);
}
}
function toggleMessageView(messageId) {
const filteredContent = document.getElementById('filtered-' + messageId);
const rawContent = document.getElementById('raw-' + messageId);
const filteredBtn = document.getElementById('filtered-btn-' + messageId);
const rawBtn = document.getElementById('raw-btn-' + messageId);
if (filteredContent.style.display === 'none') {
// Switch to filtered
filteredContent.style.display = 'block';
rawContent.style.display = 'none';
filteredBtn.classList.add('active');
rawBtn.classList.remove('active');
} else {
// Switch to raw
filteredContent.style.display = 'none';
rawContent.style.display = 'block';
rawBtn.classList.add('active');
filteredBtn.classList.remove('active');
}
}
let currentCursor = null;
let isTyping = false;
async function loadHistory(isRefresh) {
const deviceId = document.getElementById('deviceId').value;
const accessToken = document.getElementById('accessToken').value.trim();
const messagesArea = document.getElementById('messagesArea');
const loadMoreBtn = document.getElementById('loadMoreBtn');
if (!deviceId) {
alert('Please enter a Device ID');
return;
}
if (isRefresh) {
messagesArea.innerHTML = '';
currentCursor = null;
}
// Gọi API với device_id trong URL, nhưng gửi kèm headers để middleware resolve đúng identity
const url = `/api/history/${deviceId}?limit=20${currentCursor ? `&before_id=${currentCursor}` : ''}`;
// Build headers for identity resolution (middleware sẽ dùng token để override nếu có)
const headers = {
'Content-Type': 'application/json',
'device_id': deviceId
};
if (accessToken) {
headers['Authorization'] = 'Bearer ' + accessToken;
}
try {
const response = await fetch(url, { headers: headers });
const data = await response.json();
const messages = data.data || data;
const cursor = data.next_cursor || null;
if (Array.isArray(messages) && messages.length > 0) {
currentCursor = cursor;
if (isRefresh) {
// Refresh: reverse để oldest ở trên, newest ở dưới
const batch = [...messages].reverse();
batch.forEach(msg => appendMessage(msg, 'bottom'));
setTimeout(() => {
const chatBox = document.getElementById('chatBox');
chatBox.scrollTop = chatBox.scrollHeight;
}, 100);
} else {
// Load more: messages từ API theo DESC (newest first của batch cũ)
const chatBox = document.getElementById('chatBox');
const oldHeight = chatBox.scrollHeight;
for (let i = 0; i < messages.length; i++) {
appendMessage(messages[i], 'top');
}
// Adjust scroll to keep view stable
chatBox.scrollTop = chatBox.scrollHeight - oldHeight;
}
loadMoreBtn.style.display = currentCursor ? 'block' : 'none';
} else {
if (isRefresh) {
messagesArea.innerHTML = '<div class="message system">No history found. Start chatting!</div>';
}
loadMoreBtn.style.display = 'none';
}
} catch (error) {
console.error('Error loading history:', error);
alert('Failed to load history');
}
}
function appendMessage(msg, position = 'bottom') {
const messagesArea = document.getElementById('messagesArea');
// Container wrapper for alignment
const container = document.createElement('div');
container.className = `message-container ${msg.is_human ? 'user' : 'bot'}`;
// Sender Name Label
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerText = msg.is_human ? 'You' : 'Canifa AI';
container.appendChild(sender);
// Message Bubble
const div = document.createElement('div');
div.className = `message ${msg.is_human ? 'user' : 'bot'}`;
// Generate unique message ID for toggle
const messageId = 'hist-' + (msg.id || Date.now() + Math.random());
if (msg.is_human) {
// User message: simple text
div.innerText = msg.message;
} else {
// Bot message: add Widget/Raw JSON toggle
// FILTERED CONTENT (default visible)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.innerText = msg.message;
div.appendChild(filteredDiv);
// RAW CONTENT (hidden by default)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({
id: msg.id,
message: msg.message,
product_ids: msg.product_ids || [],
timestamp: msg.timestamp,
is_human: msg.is_human
}, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
div.appendChild(rawDiv);
// Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
div.appendChild(toggleDiv);
}
// Timestamp inside bubble
const time = document.createElement('span');
time.className = 'timestamp';
time.innerText = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
div.appendChild(time);
container.appendChild(div);
if (position === 'top') {
messagesArea.insertBefore(container, messagesArea.firstChild);
} else {
messagesArea.appendChild(container);
}
}
async function sendMessage() {
const input = document.getElementById('userInput');
const deviceIdInput = document.getElementById('deviceId');
const accessTokenInput = document.getElementById('accessToken');
const deviceId = deviceIdInput.value.trim();
const accessToken = accessTokenInput.value.trim();
const text = input.value.trim();
const sendBtn = document.getElementById('sendBtn');
const typingIndicator = document.getElementById('typingIndicator');
const chatBox = document.getElementById('chatBox');
if (!deviceId) {
alert('Please enter a Device ID first!');
deviceIdInput.focus();
return;
}
if (!text) return;
// Disable input
input.disabled = true;
sendBtn.disabled = true;
typingIndicator.style.display = 'block';
// Add user message immediately
appendMessage({
message: text,
is_human: true,
timestamp: new Date().toISOString(),
id: 'pending'
});
input.value = '';
chatBox.scrollTop = chatBox.scrollHeight;
// Save config to localStorage
saveConfig();
// Track response time
const startTime = Date.now();
try {
// Build headers
const headers = {
'Content-Type': 'application/json',
'device_id': deviceId
};
// Add Authorization if access token provided
if (accessToken) {
headers['Authorization'] = 'Bearer ' + accessToken;
}
const response = await fetch('/api/agent/chat', {
method: 'POST',
headers: headers,
body: JSON.stringify({
user_query: text,
device_id: deviceId
})
});
// Handle API Errors (Rate Limit, System Error) with Widget/Raw View
if (response.status === 429 || response.status === 500) {
const errorData = await response.json();
const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
const messageId = 'msg-error-' + Date.now();
// Create bot message container
const messagesArea = document.getElementById('messagesArea');
const container = document.createElement('div');
container.className = 'message-container bot';
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerText = 'Canifa AI';
container.appendChild(sender);
const botMsgDiv = document.createElement('div');
botMsgDiv.className = 'message bot';
// 1. FILTERED CONTENT (Widget View)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.style.color = '#ff6b6b';
// Extract message
const errorMessage = errorData.message ||
errorData.detail?.message ||
'Có lỗi xảy ra!';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${errorData.error_code || 'ERROR'}</div>
<div>${errorMessage}</div>
${errorData.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Đăng nhập ngay để tiếp tục!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
// 2. RAW CONTENT (JSON View)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(errorData, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// 3. Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
// Response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
container.appendChild(botMsgDiv);
messagesArea.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight;
input.disabled = false;
sendBtn.disabled = false;
typingIndicator.style.display = 'none';
return;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail?.message || errorData.detail || 'Có lỗi xảy ra');
}
const data = await response.json();
const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
// Generate unique message ID
const messageId = 'msg-' + Date.now();
// Store message data
messageHistory.push({
type: 'bot',
data: data,
responseTime: responseTime,
timestamp: new Date().toISOString(),
id: messageId
});
// Create bot message placeholder
const messagesArea = document.getElementById('messagesArea');
const container = document.createElement('div');
container.className = 'message-container bot';
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerText = 'Canifa AI';
container.appendChild(sender);
const botMsgDiv = document.createElement('div');
botMsgDiv.className = 'message bot';
if (data.status === 'success') {
// FILTERED CONTENT (default visible)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
// Display AI text response
const textDiv = document.createElement('div');
textDiv.innerText = data.ai_response || 'No response';
filteredDiv.appendChild(textDiv);
// Render product cards if available
if (data.product_ids && data.product_ids.length > 0) {
const productsContainer = document.createElement('div');
productsContainer.className = 'product-cards-container';
data.product_ids.forEach(product => {
const card = document.createElement('div');
card.className = 'product-card';
// Product image
const img = document.createElement('img');
img.src = product.thumbnail_image_url || 'https://via.placeholder.com/200';
img.alt = product.name;
img.onerror = function () { this.src = 'https://via.placeholder.com/200?text=No+Image'; };
card.appendChild(img);
// Product body
const body = document.createElement('div');
body.className = 'product-card-body';
// SKU
const sku = document.createElement('div');
sku.className = 'product-sku';
sku.innerText = product.sku;
body.appendChild(sku);
// Name
const name = document.createElement('div');
name.className = 'product-name';
name.innerText = product.name;
body.appendChild(name);
// Price
const priceDiv = document.createElement('div');
priceDiv.className = 'product-price';
if (product.sale_price && product.sale_price < product.price) {
// Show original price with strikethrough
const originalPrice = document.createElement('span');
originalPrice.className = 'price-original';
originalPrice.innerText = product.price.toLocaleString('vi-VN') + 'đ';
priceDiv.appendChild(originalPrice);
// Show sale price
const salePrice = document.createElement('span');
salePrice.className = 'price-sale';
salePrice.innerText = product.sale_price.toLocaleString('vi-VN') + 'đ';
priceDiv.appendChild(salePrice);
} else {
// Show regular price
const regularPrice = document.createElement('span');
regularPrice.className = 'price-regular';
regularPrice.innerText = product.price.toLocaleString('vi-VN') + 'đ';
priceDiv.appendChild(regularPrice);
}
body.appendChild(priceDiv);
// Link button
const link = document.createElement('a');
link.className = 'product-link';
link.href = product.url;
link.target = '_blank';
link.innerText = '🛍️ Xem chi tiết';
body.appendChild(link);
card.appendChild(body);
productsContainer.appendChild(card);
});
filteredDiv.appendChild(productsContainer);
}
botMsgDiv.appendChild(filteredDiv);
// RAW CONTENT (hidden by default)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({
status: data.status,
ai_response: data.ai_response,
product_ids: data.product_ids,
limit_info: data.limit_info || null
}, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// Add toggle button
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
// Add response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
} else {
// ERROR CASE: Limit exceeded or other errors
// FILTERED CONTENT (error message - default visible)
const filteredDiv = document.createElement('div');
filteredDiv.id = 'filtered-' + messageId;
filteredDiv.className = 'filtered-content';
filteredDiv.style.color = '#ff6b6b';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${data.error_code || 'ERROR'}</div>
<div>${data.message || 'Unknown error'}</div>
${data.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Vui lòng đăng nhập để tiếp tục sử dụng!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
// RAW CONTENT (hidden by default)
const rawDiv = document.createElement('div');
rawDiv.id = 'raw-' + messageId;
rawDiv.className = 'raw-content';
rawDiv.style.display = 'none';
const rawJsonDiv = document.createElement('div');
rawJsonDiv.className = 'raw-json-view';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({
status: data.status,
error_code: data.error_code,
message: data.message,
require_login: data.require_login,
limit_info: data.limit_info || null
}, null, 2);
rawJsonDiv.appendChild(pre);
rawDiv.appendChild(rawJsonDiv);
botMsgDiv.appendChild(rawDiv);
// Toggle Buttons
const toggleDiv = document.createElement('div');
toggleDiv.className = 'message-view-toggle';
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
toggleDiv.appendChild(rawBtn);
botMsgDiv.appendChild(toggleDiv);
}
container.appendChild(botMsgDiv);
messagesArea.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight;
} catch (error) {
console.error('Error sending message:', error);
appendMessage({
message: `Error: ${error.message}`,
is_human: false,
timestamp: new Date().toISOString(),
id: 'error'
});
} finally {
input.disabled = false;
sendBtn.disabled = false;
typingIndicator.style.display = 'none';
input.focus();
chatBox.scrollTop = chatBox.scrollHeight;
}
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
function clearUI() {
document.getElementById('messagesArea').innerHTML = '';
}
// Apply token from login prompt in rate limit error
function applyLoginToken() {
const tokenInput = document.getElementById('loginTokenInput');
if (tokenInput && tokenInput.value.trim()) {
document.getElementById('accessToken').value = tokenInput.value.trim();
saveConfig();
alert('✅ Token đã được lưu! Bạn có thể tiếp tục chat.');
} else {
alert('Vui lòng nhập Access Token!');
}
}
// Save config to localStorage (called on input change/blur)
function saveConfig() {
const deviceId = document.getElementById('deviceId').value.trim();
const accessToken = document.getElementById('accessToken').value.trim();
if (deviceId) {
localStorage.setItem('canifa_device_id', deviceId);
}
if (accessToken) {
localStorage.setItem('canifa_access_token', accessToken);
} else {
localStorage.removeItem('canifa_access_token');
}
}
// Generate UUID for device_id
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// Load config from localStorage on page load
window.onload = function () {
// Load or generate Device ID
let savedDeviceId = localStorage.getItem('canifa_device_id');
if (!savedDeviceId) {
savedDeviceId = 'device-' + generateUUID().substring(0, 8);
localStorage.setItem('canifa_device_id', savedDeviceId);
}
document.getElementById('deviceId').value = savedDeviceId;
// Load Access Token (optional)
const savedAccessToken = localStorage.getItem('canifa_access_token');
if (savedAccessToken) {
document.getElementById('accessToken').value = savedAccessToken;
}
// Auto-load history
setTimeout(() => loadHistory(true), 50);
};
</script>
</div> <!-- Close main-content -->
</body>
</html> -->
\ No newline at end of file
# check_server.py
import pymysql
conn = pymysql.connect(
host="172.16.2.100", port=9030, user="anhvh", password="v0WYGeyLRCckXotT", database="shared_source"
)
cursor = conn.cursor()
# Check max connections
cursor.execute("SHOW VARIABLES LIKE 'max_connections'")
print("Max Connections:", cursor.fetchone())
# Check current connections
cursor.execute("SHOW PROCESSLIST")
processes = cursor.fetchall()
print(f"Current Active Connections: {len(processes)}")
# Check slow queries
cursor.execute("SHOW VARIABLES LIKE 'long_query_time'")
print("Slow Query Threshold:", cursor.fetchone())
conn.close()
# quick_test.py
import time
import pymysql
def quick_test():
print("🔍 Testing StarRocks connection...")
print("=" * 50)
print("\n📡 MySQL Connection Latency Test:")
latencies = []
for i in range(10):
start = time.time()
try:
conn = pymysql.connect(
host="172.16.2.100",
port=9030,
user="anhvh",
password="v0WYGeyLRCckXotT",
database="shared_source",
connect_timeout=10
)
latency = (time.time() - start) * 1000
latencies.append(latency)
print(f" ✅ Attempt {i+1}: {latency:.2f}ms")
# Lần đầu tiên thì check connection limits
if i == 0:
cursor = conn.cursor()
cursor.execute("SHOW VARIABLES LIKE 'max_connections'")
max_conn = cursor.fetchone()
cursor.execute("SHOW STATUS LIKE 'Threads_connected'")
current = cursor.fetchone()
cursor.execute("SHOW STATUS LIKE 'Max_used_connections'")
max_used = cursor.fetchone()
print(f"\n🔌 Connection Limits:")
print(f" Max Connections: {max_conn[1] if max_conn else 'N/A'}")
print(f" Current Active: {current[1] if current else 'N/A'}")
print(f" Peak Ever Used: {max_used[1] if max_used else 'N/A'}")
# Tính % usage
if max_conn and current:
usage = (int(current[1]) / int(max_conn[1])) * 100
print(f" Usage: {usage:.1f}%")
if usage > 80:
print(" ⚠️ WARNING: Connection pool > 80% full!")
conn.close()
except Exception as e:
print(f" ❌ Attempt {i+1} Failed: {e}")
time.sleep(0.3)
if latencies:
print(f"\n📊 Summary:")
print(f" Average: {sum(latencies)/len(latencies):.2f}ms")
print(f" Min: {min(latencies):.2f}ms")
print(f" Max: {max(latencies):.2f}ms")
if __name__ == "__main__":
quick_test()
\ No newline at end of file
import requests
import json
url = "http://172.16.2.207:5000/api/agent/chat"
print(f"\n--- TESTING DEVICE ID IN BODY ---")
# Payload có chứa device_id
payload = {
"user_query": "hello test device id in body",
"device_id": "device-id-from-body-test"
}
# Header KHÔNG có device-id
headers = {
"Content-Type": "application/json"
}
try:
res = requests.post(url, json=payload, headers=headers, timeout=10)
if res.status_code == 200:
data = res.json()
limit = data.get('limit_info', {}).get('limit')
used = data.get('limit_info', {}).get('used')
print(f"✅ Status: {res.status_code}")
print(f"ℹ️ Limit: {limit} | Used: {used}")
# Nếu logic đúng, nó phải nhận ra device_id này và trả về limit = 10 (Guest)
# Nếu logic sai (không đọc được body), nó sẽ fallback về 'unknown' (cũng limit 10)
# Để chắc chắn, ta check xem limit đã bị trừ chưa (used > 0)
# Nhưng device_id unknown cũng được tính limit riêng.
# Ta có thể check log server, nhưng ở đây ta check limit behavior.
if limit == 10:
print("✅ Server recognized Guest mode (likely from body device_id).")
else:
print(f"⚠️ Unexpected limit: {limit}")
else:
print(f"❌ Failed: {res.status_code}")
print(res.text[:100])
except Exception as e:
print(f"❌ Error: {e}")
import requests
import json
url = "http://172.16.2.207:5000/api/agent/chat"
def test_request(name, headers):
print(f"\n--- TESTING {name} ---")
payload = {"user_query": "hello test user identify"}
try:
res = requests.post(url, json=payload, headers=headers, timeout=10)
if res.status_code == 200:
data = res.json()
limit = data.get('limit_info', {}).get('limit')
used = data.get('limit_info', {}).get('used')
print(f"✅ Status: {res.status_code}")
print(f"ℹ️ Limit: {limit} | Used: {used}")
print(f"ℹ️ Identity Check: {'Authenticated' if limit == 100 else 'Guest'}")
else:
print(f"❌ Failed: {res.status_code}")
print(res.text[:100])
except Exception as e:
print(f"❌ Error: {e}")
# 1. Guest (Chỉ có Device ID)
test_request("GUEST (Device ID Only)", {
"Content-Type": "application/json",
"device-id": "guest-device-final-check"
})
# 2. Authenticated (Token + Device ID)
test_request("AUTHENTICATED (Token)", {
"Content-Type": "application/json",
"Authorization": "Bearer 071w198x23ict4hs1i6bl889fit5p3f7",
"device-id": "guest-device-final-check"
})
import requests
import json
import time
url = "http://172.16.2.207:5000/api/agent/chat"
token = "071w198x23ict4hs1i6bl889fit5p3f7"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"device-id": "remote-check-01"
}
payload = {
"user_query": "hello, test server connection"
}
print(f"Testing connectivity to REMOTE server: {url}")
start = time.time()
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
print(f"Status Code: {response.status_code}")
print(f"Time taken: {time.time() - start:.2f}s")
if response.status_code == 200:
print("✅ Connection Successful!")
print("Response Preview:", str(response.json())[:200])
else:
print("❌ Server Error:")
print(response.text)
except Exception as e:
print(f"❌ Connection Failed: {e}")
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