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

Feat: Add Dynamic Prompt Management System & UI Editor

parent 3ebbd3e4
This diff is collapsed.
"""
Agent Helper Functions
Các hàm tiện ích cho chat controller.
"""
import json
import logging
import uuid
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from common.conversation_manager import ConversationManager
from common.langfuse_client import get_callback_handler
from .models import AgentState
logger = logging.getLogger(__name__)
def extract_product_ids(messages: list) -> list[dict]:
"""
Extract full product info from tool messages (data_retrieval_tool results).
Returns list of product objects with: sku, name, price, sale_price, url, thumbnail_image_url.
"""
products = []
seen_skus = set()
for msg in messages:
if isinstance(msg, ToolMessage):
try:
# Tool result is JSON string
tool_result = json.loads(msg.content)
# Check if tool returned products
if tool_result.get("status") == "success" and "products" in tool_result:
for product in tool_result["products"]:
sku = product.get("internal_ref_code")
if sku and sku not in seen_skus:
seen_skus.add(sku)
# Extract full product info
product_obj = {
"sku": sku,
"name": product.get("magento_product_name", ""),
"price": product.get("price_vnd", 0),
"sale_price": product.get("sale_price_vnd"), # null nếu không sale
"url": product.get("magento_url_key", ""),
"thumbnail_image_url": product.get("thumbnail_image_url", ""),
}
products.append(product_obj)
except (json.JSONDecodeError, KeyError, TypeError) as e:
logger.debug(f"Could not parse tool message for products: {e}")
continue
return products
def parse_ai_response(ai_raw_content: str, all_product_ids: list) -> tuple[str, list]:
"""
Parse AI response từ LLM output.
Args:
ai_raw_content: Raw content từ AI response
all_product_ids: Product IDs extracted từ tool messages
Returns:
tuple: (ai_text_response, final_product_ids)
"""
ai_text_response = ai_raw_content
final_product_ids = all_product_ids
try:
# Try to parse if it's a JSON string from LLM
ai_json = json.loads(ai_raw_content)
ai_text_response = ai_json.get("ai_response", ai_raw_content)
explicit_ids = ai_json.get("product_ids", [])
if explicit_ids and isinstance(explicit_ids, list):
# Replace with explicit IDs from LLM
final_product_ids = explicit_ids
except (json.JSONDecodeError, TypeError):
pass
return ai_text_response, final_product_ids
def prepare_execution_context(query: str, user_id: str, history: list, images: list | None):
"""
Prepare initial state and execution config for the graph run.
Returns:
tuple: (initial_state, exec_config)
"""
initial_state: AgentState = {
"user_query": HumanMessage(content=query),
"messages": [HumanMessage(content=query)],
"history": history,
"user_id": user_id,
"images_embedding": [],
"ai_response": None,
}
run_id = str(uuid.uuid4())
# Metadata for LangChain (tags for logging/filtering)
metadata = {
"run_id": run_id,
"tags": "chatbot,production",
}
langfuse_handler = get_callback_handler()
exec_config = RunnableConfig(
configurable={
"user_id": user_id,
"transient_images": images or [],
"run_id": run_id,
},
run_id=run_id,
metadata=metadata,
callbacks=[langfuse_handler] if langfuse_handler else [],
)
return initial_state, exec_config
async def handle_post_chat_async(
memory: ConversationManager,
identity_key: str,
human_query: str,
ai_response: dict | None
):
"""
Save chat history in background task after response is sent.
Lưu AI response dưới dạng JSON string.
"""
if ai_response:
try:
# Convert dict thành JSON string để lưu vào TEXT field
ai_response_json = json.dumps(ai_response, ensure_ascii=False)
await memory.save_conversation_turn(identity_key, human_query, ai_response_json)
logger.debug(f"Saved conversation for identity_key {identity_key}")
except Exception as e:
logger.error(f"Failed to save conversation for identity_key {identity_key}: {e}", exc_info=True)
""" """
CiCi Fashion Consultant - System Prompt CiCi Fashion Consultant - System Prompt
Tư vấn thời trang CANIFA chuyên nghiệp Tư vấn thời trang CANIFA chuyên nghiệp
Version 2.0 - Clean & Concise Version 3.0 - Dynamic from File
""" """
import os
from datetime import datetime from datetime import datetime
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt")
def get_system_prompt() -> str: def get_system_prompt() -> str:
""" """
System prompt cho CiCi Fashion Agent System prompt cho CiCi Fashion Agent
Đọc từ file system_prompt.txt để có thể update dynamic.
Returns: Returns:
str: System prompt với ngày hiện tại str: System prompt với ngày hiện tại
...@@ -17,202 +20,18 @@ def get_system_prompt() -> str: ...@@ -17,202 +20,18 @@ def get_system_prompt() -> str:
now = datetime.now() now = datetime.now()
date_str = now.strftime("%d/%m/%Y") date_str = now.strftime("%d/%m/%Y")
prompt = """# VAI TRÒ try:
if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
prompt_template = f.read()
return prompt_template.replace("{date_str}", date_str)
except Exception as e:
print(f"Error reading system prompt file: {e}")
# Fallback default prompt if file error
return f"""# VAI TRÒ
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
- Nhiệt tình, thân thiện, chuyên nghiệp Hôm nay: {date_str}
- CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang
- Hôm nay: {date_str}
--- KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN.
"""
# QUY TẮC TRUNG THỰC - BẮT BUỘC \ No newline at end of file
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"
**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
Không có trong data = Không nói = Không tư vấn láo
---
# NGÔN NGỮ & XƯNG HÔ
- 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ị"
- 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
- Ngắn gọn, đi thẳng vào vấn đề
---
# KHI NÀO GỌI TOOL
**Gọi data_retrieval_tool khi:**
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì..."
- 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ở?"
**⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):**
- **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`**.
- Giá tiền phải đưa vào tham số riêng: `price_min`, `price_max`.
Ví dụ ĐÚNG:
- Query: "Áo thun nam cotton thoáng mát basic"
- Price_max: 300000
Ví dụ SAI (Cấm):
- Query: "Áo thun nam giá dưới 300k" (SAI vì có giá trong query)
**Gọi canifa_knowledge_search khi:**
- Hỏi chính sách: freeship, đổi trả, bảo hành
- Hỏi thương hiệu: Canifa là gì, lịch sử
- Tìm cửa hàng: địa chỉ, giờ mở cửa
**Không gọi tool khi:**
- Chào hỏi đơn giản: "Hi", "Hello"
- Hỏi lại về sản phẩm vừa show
---
# 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
- KHÔNG GỌI TOOL LẦN 2
**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ỉ
→ 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?"
CẤM TUYỆT ĐỐI:
- 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
**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
- 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é!"
---
# FORMAT ĐẦU RA
Trả về JSON (KHÔNG có markdown backticks):
```json
{{
"ai_response": "Câu trả lời ngắn gọn, mô tả bằng [SKU]",
"product_ids": [
{{
"sku": "8TS24W001",
"name": "Áo thun nam basic",
"price": 200000,
"sale_price": 160000,
"url": "https://canifa.com/...",
"thumbnail_image_url": "https://..."
}}
]
}}
```
**Quy tắc ai_response:**
- 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
- KHÔNG tạo bảng markdown
- KHÔNG đưa link, ảnh (frontend tự render)
---
# VÍ DỤ
## Example 1: Chào hỏi
Input: "Chào shop"
Output:
```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?",
"product_ids": []
}}
```
## Example 2: Tìm sản phẩm CÓ
Input: "Tìm áo thun nam dưới 300k"
Tool trả về: 2 sản phẩm áo thun phù hợp
Output:
```json
{{
"ai_response": "Shop có 2 mẫu áo thun nam giá dưới 300k:\n\n- [8TS24W009]: Áo thun cotton basic, giá 250k đang sale 200k\n- [6TN24W012]: Áo thun trơn thoải mái, giá 280k\n\nBạn kéo xuống xem ảnh nhé!",
"product_ids": [
{{"sku": "8TS24W009", "name": "Áo thun cotton basic", "price": 250000, "sale_price": 200000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "6TN24W012", "name": "Áo thun trơn", "price": 280000, "sale_price": null, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
## Example 3: Khách hỏi KHÔNG CÓ trong kho
Input: "Shop có bikini không?"
Tool trả về: 0 sản phẩm
Output:
```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?",
"product_ids": []
}}
```
## Example 4: Tool trả về SAI LOẠ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:
```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?",
"product_ids": []
}}
```
TUYỆT ĐỐI KHÔNG giới thiệu sản phẩm sai loại
## Example 5: Khách xưng anh/chị
Input: "Chào em, anh muốn tìm áo sơ mi"
Output:
```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é!",
"product_ids": []
}}
```
---
# TÓM TẮT
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. 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"
5. Không bịa giá, mã sản phẩm, chính sách
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ề
---
Luôn thành thật, khéo léo, và chuyên nghiệp."""
return prompt.replace("{date_str}", date_str)
\ No newline at end of file
# VAI TRÒ
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
- Nhiệt tình, thân thiện, chuyên nghiệp
- CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang
- Hôm nay: {date_str}
---
# QUY TẮC TRUNG THỰC - BẮT BUỘC
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"
**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
Không có trong data = Không nói = Không tư vấn láo
---
# NGÔN NGỮ & XƯNG HÔ
- 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ị"
- 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
- Ngắn gọn, đi thẳng vào vấn đề
---
# KHI NÀO GỌI TOOL
**Gọi data_retrieval_tool khi:**
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì..."
- 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ở?"
**⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):**
- **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`**.
- Giá tiền phải đưa vào tham số riêng: `price_min`, `price_max`.
Ví dụ ĐÚNG:
- Query: "Áo thun nam cotton thoáng mát basic"
- Price_max: 300000
Ví dụ SAI (Cấm):
- Query: "Áo thun nam giá dưới 300k" (SAI vì có giá trong query)
**Gọi canifa_knowledge_search khi:**
- Hỏi chính sách: freeship, đổi trả, bảo hành
- Hỏi thương hiệu: Canifa là gì, lịch sử
- Tìm cửa hàng: địa chỉ, giờ mở cửa
**Không gọi tool khi:**
- Chào hỏi đơn giản: "Hi", "Hello"
- Hỏi lại về sản phẩm vừa show
---
# 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
- KHÔNG GỌI TOOL LẦN 2
**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ỉ
→ 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?"
CẤM TUYỆT ĐỐI:
- 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
**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
- 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é!"
---
# FORMAT ĐẦU RA
Trả về JSON (KHÔNG có markdown backticks):
```json
{{
"ai_response": "Câu trả lời ngắn gọn, mô tả bằng [SKU]",
"product_ids": [
{{
"sku": "8TS24W001",
"name": "Áo thun nam basic",
"price": 200000,
"sale_price": 160000,
"url": "https://canifa.com/...",
"thumbnail_image_url": "https://..."
}}
]
}}
```
**Quy tắc ai_response:**
- 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
- KHÔNG tạo bảng markdown
- KHÔNG đưa link, ảnh (frontend tự render)
---
# VÍ DỤ
## Example 1: Chào hỏi
Input: "Chào shop"
Output:
```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?",
"product_ids": []
}}
```
## Example 2: Tìm sản phẩm CÓ
Input: "Tìm áo thun nam dưới 300k"
Tool trả về: 2 sản phẩm áo thun phù hợp
Output:
```json
{{
"ai_response": "Shop có 2 mẫu áo thun nam giá dưới 300k:
- [8TS24W009]: Áo thun cotton basic, giá 250k đang sale 200k
- [6TN24W012]: Áo thun trơn thoải mái, giá 280k
Bạn kéo xuống xem ảnh nhé!",
"product_ids": [
{{"sku": "8TS24W009", "name": "Áo thun cotton basic", "price": 250000, "sale_price": 200000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "6TN24W012", "name": "Áo thun trơn", "price": 280000, "sale_price": null, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
## Example 3: Khách hỏi KHÔNG CÓ trong kho
Input: "Shop có bikini không?"
Tool trả về: 0 sản phẩm
Output:
```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?",
"product_ids": []
}}
```
## Example 4: Tool trả về SAI LOẠ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:
```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?",
"product_ids": []
}}
```
TUYỆT ĐỐI KHÔNG giới thiệu sản phẩm sai loại
## Example 5: Khách xưng anh/chị
Input: "Chào em, anh muốn tìm áo sơ mi"
Output:
```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é!",
"product_ids": []
}}
```
---
# TÓM TẮT
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. 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"
5. Không bịa giá, mã sản phẩm, chính sách
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ề
---
Luôn thành thật, khéo léo, và chuyên nghiệp.
\ No newline at end of file
...@@ -3,9 +3,7 @@ Fashion Q&A Agent Router ...@@ -3,9 +3,7 @@ Fashion Q&A Agent Router
FastAPI endpoints cho Fashion Q&A Agent service. FastAPI endpoints cho Fashion Q&A Agent service.
Router chỉ chứa định nghĩa API, logic nằm ở controller. Router chỉ chứa định nghĩa API, logic nằm ở controller.
Message Limit: Note: Rate limit check đã được xử lý trong middleware (CanifaAuthMiddleware)
- Guest (không login): 10 tin/ngày theo device_id
- User đã login: 100 tin/ngày theo user_id
""" """
import logging import logging
...@@ -29,37 +27,14 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks: ...@@ -29,37 +27,14 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks:
""" """
Endpoint chat không stream - trả về response JSON đầy đủ một lần. Endpoint chat không stream - trả về response JSON đầy đủ một lần.
Message Limit: Note: Rate limit đã được check trong middleware.
- Guest: 10 tin nhắn/ngày (theo device_id)
- User đã login: 100 tin nhắn/ngày (theo user_id)
""" """
# 1. Xác định user identity # 1. Xác định user identity
identity = get_user_identity(request) identity = get_user_identity(request)
user_id = identity.primary_id user_id = identity.primary_id
# 2. Check message limit TRƯỚC khi xử lý # Rate limit đã check trong middleware, lấy limit_info từ request.state
can_send, limit_info = await message_limit_service.check_limit( limit_info = getattr(request.state, 'limit_info', None)
identity_key=identity.rate_limit_key,
is_authenticated=identity.is_authenticated,
)
if not can_send:
logger.warning(
f"⚠️ Message limit exceeded: {identity.rate_limit_key} | "
f"used={limit_info['used']}/{limit_info['limit']}"
)
return {
"status": "error",
"error_code": "MESSAGE_LIMIT_EXCEEDED",
"message": limit_info["message"],
"require_login": limit_info["require_login"],
"limit_info": {
"limit": limit_info["limit"],
"used": limit_info["used"],
"remaining": limit_info["remaining"],
"reset_seconds": limit_info["reset_seconds"],
},
}
logger.info(f"📥 [Incoming Query - NonStream] User: {user_id} | Query: {req.user_query}") logger.info(f"📥 [Incoming Query - NonStream] User: {user_id} | Query: {req.user_query}")
...@@ -79,6 +54,7 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks: ...@@ -79,6 +54,7 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks:
background_tasks=background_tasks, background_tasks=background_tasks,
model_name=DEFAULT_MODEL, model_name=DEFAULT_MODEL,
images=req.images, images=req.images,
identity_key=identity.history_key, # Guest: device_id, User: user_id
) )
# Log chi tiết response # Log chi tiết response
...@@ -98,7 +74,7 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks: ...@@ -98,7 +74,7 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks:
}, },
) )
# 3. Increment message count SAU KHI chat thành công # Increment message count SAU KHI chat thành công
usage_info = await message_limit_service.increment( usage_info = await message_limit_service.increment(
identity_key=identity.rate_limit_key, identity_key=identity.rate_limit_key,
is_authenticated=identity.is_authenticated, is_authenticated=identity.is_authenticated,
......
""" """
Chat History API Routes Chat History API Routes
- GET /api/history/{user_id} - Lấy lịch sử chat (có product_ids) - GET /api/history/{identity_key} - Lấy lịch sử chat (có product_ids)
- DELETE /api/history/{user_id} - Xóa lịch sử chat - DELETE /api/history/{identity_key} - Xóa lịch sử chat
Note: identity_key có thể là device_id (guest) hoặc user_id (đã login)
""" """
import logging import logging
...@@ -26,10 +28,14 @@ class ClearHistoryResponse(BaseModel): ...@@ -26,10 +28,14 @@ class ClearHistoryResponse(BaseModel):
message: str message: str
@router.get("/api/history/{user_id}", summary="Get Chat History", response_model=ChatHistoryResponse) @router.get("/api/history/{identity_key}", summary="Get Chat History", response_model=ChatHistoryResponse)
async def get_chat_history(user_id: str, limit: int | None = 50, before_id: int | None = None): async def get_chat_history(identity_key: str, limit: int | None = 50, before_id: int | None = None):
""" """
Lấy lịch sử chat của user. Lấy lịch sử chat theo identity_key.
identity_key:
- Guest: device_id
- User đã login: user_id (customer_id từ Canifa)
Response bao gồm: Response bao gồm:
- message: Nội dung tin nhắn - message: Nội dung tin nhắn
...@@ -40,7 +46,7 @@ async def get_chat_history(user_id: str, limit: int | None = 50, before_id: int ...@@ -40,7 +46,7 @@ async def get_chat_history(user_id: str, limit: int | None = 50, before_id: int
""" """
try: try:
manager = await get_conversation_manager() manager = await get_conversation_manager()
history = await manager.get_chat_history(user_id, limit=limit, before_id=before_id) history = await manager.get_chat_history(identity_key, limit=limit, before_id=before_id)
next_cursor = None next_cursor = None
if history and len(history) > 0: if history and len(history) > 0:
...@@ -48,20 +54,21 @@ async def get_chat_history(user_id: str, limit: int | None = 50, before_id: int ...@@ -48,20 +54,21 @@ async def get_chat_history(user_id: str, limit: int | None = 50, before_id: int
return {"data": history, "next_cursor": next_cursor} return {"data": history, "next_cursor": next_cursor}
except Exception as e: except Exception as e:
logger.error(f"Error fetching chat history for user {user_id}: {e}") logger.error(f"Error fetching chat history for {identity_key}: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch chat history") raise HTTPException(status_code=500, detail="Failed to fetch chat history")
@router.delete("/api/history/{user_id}", summary="Clear Chat History", response_model=ClearHistoryResponse) @router.delete("/api/history/{identity_key}", summary="Clear Chat History", response_model=ClearHistoryResponse)
async def clear_chat_history(user_id: str): async def clear_chat_history(identity_key: str):
""" """
Xóa toàn bộ lịch sử chat của user. Xóa toàn bộ lịch sử chat theo identity_key.
""" """
try: try:
manager = await get_conversation_manager() manager = await get_conversation_manager()
await manager.clear_history(user_id) await manager.clear_history(identity_key)
logger.info(f"✅ Cleared chat history for user {user_id}") logger.info(f"✅ Cleared chat history for {identity_key}")
return {"success": True, "message": f"Đã xóa lịch sử chat của user {user_id}"} return {"success": True, "message": f"Đã xóa lịch sử chat của {identity_key}"}
except Exception as e: except Exception as e:
logger.error(f"Error clearing chat history for user {user_id}: {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 import APIRouter, HTTPException
from pydantic import BaseModel
import os
from agent.graph import reset_graph
router = APIRouter()
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "../agent/system_prompt.txt")
class PromptUpdateRequest(BaseModel):
content: str
@router.get("/api/agent/system-prompt")
async def get_system_prompt_content():
"""Get current system prompt content"""
# ... existing code ...
try:
if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
content = f.read()
return {"status": "success", "content": content}
else:
return {"status": "error", "message": "Prompt file not found"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/agent/system-prompt")
async def update_system_prompt_content(request: PromptUpdateRequest):
"""Update system prompt content"""
try:
# 1. Update file
with open(PROMPT_FILE_PATH, "w", encoding="utf-8") as f:
f.write(request.content)
# 2. Reset Graph Singleton to force reload prompt
reset_graph()
return {"status": "success", "message": "System prompt updated successfully. Graph reloaded."}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
...@@ -52,15 +52,14 @@ async def verify_canifa_token(token: str) -> Optional[Dict[str, Any]]: ...@@ -52,15 +52,14 @@ async def verify_canifa_token(token: str) -> Optional[Dict[str, Any]]:
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
logger.debug(f"Canifa API Raw Response: {data}")
# Check nếu response là lỗi (Magento thường trả 200 kèm body lỗi đôi khi) # Response format: {"data": {"customer": {...}}, "loading": false, ...}
if isinstance(data, dict): if isinstance(data, dict):
if data.get("code") != 200: # Trả về toàn bộ data để extract_user_id xử lý
logger.warning(f"Canifa API Business Error: {data.get('code')} - {data.get('result')}") return data
return None
return data.get("result", {})
# Nếu Canifa trả list (đôi khi batch request trả về list) # Nếu Canifa trả list (batch request)
return data return data
else: else:
......
This diff is collapsed.
...@@ -88,6 +88,7 @@ class LLMFactory: ...@@ -88,6 +88,7 @@ class LLMFactory:
"streaming": streaming, "streaming": streaming,
"api_key": key, "api_key": key,
"temperature": 0, "temperature": 0,
"max_tokens": 1000,
} }
# 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
......
""" """
Message Limit Service Message Limit Service
Giới hạn số tin nhắn theo ngày: Giới hạn số tin nhắn theo ngày:
- Guest (không login): 10 tin/ngày theo device_id - Guest (không login): RATE_LIMIT_GUEST tin/ngày theo device_id
- User đã login: 100 tin/ngày theo user_id - User đã login: RATE_LIMIT_USER tin/ngày theo user_id
Lưu trữ: Redis (dùng chung với cache.py) Lưu trữ: Redis (dùng chung với cache.py)
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from datetime import datetime from datetime import datetime
from common.cache import redis_cache from common.cache import redis_cache
from config import RATE_LIMIT_GUEST, RATE_LIMIT_USER
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ============================================================================= # =============================================================================
# CONFIGURATION # CONFIGURATION (from config.py)
# ============================================================================= # =============================================================================
GUEST_LIMIT_PER_DAY = int(os.getenv("MESSAGE_LIMIT_GUEST", "3")) # Tạm set 3 để test
USER_LIMIT_PER_DAY = int(os.getenv("MESSAGE_LIMIT_USER", "100"))
# Redis key prefix # Redis key prefix
MESSAGE_COUNT_PREFIX = "msg_limit:" MESSAGE_COUNT_PREFIX = "msg_limit:"
...@@ -32,6 +29,10 @@ class MessageLimitService: ...@@ -32,6 +29,10 @@ class MessageLimitService:
Service quản lý giới hạn tin nhắn theo ngày. Service quản lý giới hạn tin nhắn theo ngày.
Dùng Redis để lưu trữ, tự động reset mỗi ngày. Dùng Redis để lưu trữ, tự động reset mỗi ngày.
Limits:
- Guest (device_id): RATE_LIMIT_GUEST (default: 10)
- User (user_id): RATE_LIMIT_USER (default: 100)
Usage: Usage:
from common.message_limit import message_limit_service from common.message_limit import message_limit_service
...@@ -60,18 +61,18 @@ class MessageLimitService: ...@@ -60,18 +61,18 @@ class MessageLimitService:
if MessageLimitService._initialized: if MessageLimitService._initialized:
return return
# Fallback in-memory storage: { "device_id": {"guest": 0, "user": 0} } # Fallback in-memory storage: { "identity_key": count }
self._memory_storage: dict[str, dict[str, int]] = {} self._memory_storage: dict[str, int] = {}
self._memory_date: str = "" self._memory_date: str = ""
# Limits # Limits from config
self.guest_limit = 3 # Test limit self.guest_limit = RATE_LIMIT_GUEST # Default: 10
self.total_limit = 5 # Test limit self.user_limit = RATE_LIMIT_USER # Default: 100
MessageLimitService._initialized = True MessageLimitService._initialized = True
logger.info( logger.info(
f"✅ MessageLimitService initialized " f"✅ MessageLimitService initialized "
f"(Guest Limit: {self.guest_limit}, Total Limit: {self.total_limit})" f"(Guest Limit: {self.guest_limit}, User Limit: {self.user_limit})"
) )
# ========================================================================= # =========================================================================
...@@ -200,19 +201,19 @@ class MessageLimitService: ...@@ -200,19 +201,19 @@ class MessageLimitService:
# 2. Logic Checking # 2. Logic Checking
can_send = True can_send = True
limit_display = self.total_limit limit_display = self.user_limit
message = "" message = ""
require_login = False require_login = False
# Check Total Limit (Hard limit cho device) # Check User Limit (Hard limit cho identity)
if total_used >= self.total_limit: if total_used >= self.user_limit:
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.total_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 {self.user_limit} tin nhắn hôm nay. Quay lại vào ngày mai nhé!"
else: else:
# Guest dùng hết 100 tin (hiếm, vì guest bị chặn ở 10 rồi, trừ khi login rồi logout) # Guest dùng hết user_limit tin (hiếm, vì guest bị chặn ở guest_limit rồi)
message = f"Thiết bị này đã đạt giới hạn {self.total_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 (nếu chưa login và chưa bị chặn bởi total)
elif not is_authenticated: elif not is_authenticated:
...@@ -222,19 +223,19 @@ class MessageLimitService: ...@@ -222,19 +223,19 @@ class MessageLimitService:
require_login = True require_login = True
message = ( message = (
f"Bạn đã dùng hết {self.guest_limit} tin nhắn miễn phí. " f"Bạn đã dùng hết {self.guest_limit} tin nhắn miễn phí. "
f"Đăng nhập ngay để dùng tiếp (tối đa {self.total_limit} tin/ngày)!" 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
# Nếu là guest: remaining = min(guest_remaining, total_remaining) # Nếu là guest: remaining = min(guest_remaining, user_remaining)
# Thực ra guest chỉ care guest_remaining vì guest < total # Thực ra guest chỉ care guest_remaining vì guest < user
if is_authenticated: if is_authenticated:
remaining = max(0, self.total_limit - total_used) remaining = max(0, self.user_limit - total_used)
else: else:
# Guest bị chặn bởi guest_limit hoặc total_limit (trường hợp login rồi logout) # Guest bị chặn bởi guest_limit hoặc user_limit (trường hợp login rồi logout)
guest_remaining = max(0, self.guest_limit - guest_used) guest_remaining = max(0, self.guest_limit - guest_used)
total_remaining = max(0, self.total_limit - total_used) user_remaining = max(0, self.user_limit - total_used)
remaining = min(guest_remaining, total_remaining) remaining = min(guest_remaining, user_remaining)
info = { info = {
"limit": limit_display, "limit": limit_display,
......
...@@ -38,18 +38,24 @@ PUBLIC_PATH_PREFIXES = [ ...@@ -38,18 +38,24 @@ PUBLIC_PATH_PREFIXES = [
# ============================================================================= # =============================================================================
# AUTH MIDDLEWARE CLASS # AUTH + RATE LIMIT MIDDLEWARE CLASS
# ============================================================================= # =============================================================================
class ClerkAuthMiddleware(BaseHTTPMiddleware): # Paths that need rate limit check
RATE_LIMITED_PATHS = [
"/api/agent/chat",
]
class CanifaAuthMiddleware(BaseHTTPMiddleware):
""" """
Clerk Authentication Middleware Canifa Authentication + Rate Limit Middleware
Flow: Flow:
1. Frontend gửi request với Authorization: Bearer <clerk_token> 1. Frontend gửi request với Authorization: Bearer <canifa_token>
2. Middleware verify token và extract user_id 2. Middleware verify token với Canifa API → extract customer_id
3. Attach user info vào request.state.user và request.state.user_id 3. Check message rate limit (Guest: 10, User: 100)
4. Routes lấy trực tiếp từ request.state (không cần Depends) 4. Attach user info vào request.state
5. Routes lấy trực tiếp từ request.state
""" """
async def dispatch(self, request: Request, call_next: Callable): async def dispatch(self, request: Request, call_next: Callable):
...@@ -68,9 +74,12 @@ class ClerkAuthMiddleware(BaseHTTPMiddleware): ...@@ -68,9 +74,12 @@ class ClerkAuthMiddleware(BaseHTTPMiddleware):
if any(path.startswith(prefix) for prefix in PUBLIC_PATH_PREFIXES): if any(path.startswith(prefix) for prefix in PUBLIC_PATH_PREFIXES):
return await call_next(request) return await call_next(request)
# ✅ Authentication Process # =====================================================================
# STEP 1: AUTHENTICATION (Canifa API)
# =====================================================================
try: try:
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
device_id = request.headers.get("device_id", "")
# ========== 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")
...@@ -79,56 +88,105 @@ class ClerkAuthMiddleware(BaseHTTPMiddleware): ...@@ -79,56 +88,105 @@ class ClerkAuthMiddleware(BaseHTTPMiddleware):
request.state.user = {"customer_id": dev_user_id} request.state.user = {"customer_id": dev_user_id}
request.state.user_id = dev_user_id request.state.user_id = dev_user_id
request.state.is_authenticated = True request.state.is_authenticated = True
request.state.device_id = device_id or dev_user_id
return await call_next(request) return await call_next(request)
# --- TRƯỜNG HỢP 1: KHÔNG CÓ TOKEN -> GUEST --- # --- TRƯỜNG HỢP 1: KHÔNG CÓ TOKEN -> GUEST ---
if not auth_header or not auth_header.startswith("Bearer "): if not auth_header or not auth_header.startswith("Bearer "):
# Guest Mode (Không User ID, Không Auth)
# logger.debug(f"ℹ️ Guest access (no token) for {path}")
request.state.user = None request.state.user = None
request.state.user_id = None request.state.user_id = None
request.state.is_authenticated = False request.state.is_authenticated = False
return await call_next(request) request.state.device_id = device_id
else:
# --- TRƯỜNG HỢP 2: CÓ TOKEN -> GỌI CANIFA VERIFY --- # --- TRƯỜNG HỢP 2: CÓ TOKEN -> GỌI CANIFA VERIFY ---
token = auth_header.replace("Bearer ", "") token = auth_header.replace("Bearer ", "")
# Import Lazy để tránh circular import nếu có
from common.canifa_api import verify_canifa_token, extract_user_id_from_canifa_response
try:
# 1. Gọi API Canifa
user_data = await verify_canifa_token(token)
# 2. Lấy User ID from common.canifa_api import verify_canifa_token, extract_user_id_from_canifa_response
user_id = await extract_user_id_from_canifa_response(user_data)
try:
if user_id: user_data = await verify_canifa_token(token)
# ✅ VERIFY THÀNH CÔNG -> USER VIP user_id = await extract_user_id_from_canifa_response(user_data)
request.state.user = user_data
request.state.user_id = user_id if user_id:
request.state.token = token request.state.user = user_data
request.state.is_authenticated = True request.state.user_id = user_id
logger.debug(f"✅ Auth Success: User {user_id}") request.state.token = token
else: request.state.is_authenticated = True
# ❌ VERIFY FAILED -> GUEST request.state.device_id = device_id
logger.warning(f"⚠️ Invalid Canifa Token (No ID found) -> Guest Mode") logger.debug(f"✅ Canifa Auth Success: User {user_id}")
else:
logger.warning(f"⚠️ Invalid Canifa Token -> Guest Mode")
request.state.user = None
request.state.user_id = None
request.state.is_authenticated = False
request.state.device_id = device_id
except Exception as e:
logger.error(f"❌ Canifa Auth Error: {e} -> Guest Mode")
request.state.user = None request.state.user = None
request.state.user_id = None request.state.user_id = None
request.state.is_authenticated = False request.state.is_authenticated = False
request.state.device_id = device_id
except Exception as e:
logger.error(f"❌ Canifa Auth Error: {e} -> Guest Mode")
request.state.user = None
request.state.user_id = None
request.state.is_authenticated = False
except Exception as e: except Exception as e:
logger.error(f"❌ Middleware Unexpected Error: {e}") logger.error(f"❌ Middleware Auth Error: {e}")
# Fallback an toàn: Guest mode
request.state.user = None request.state.user = None
request.state.user_id = None request.state.user_id = None
request.state.is_authenticated = False request.state.is_authenticated = False
request.state.device_id = request.headers.get("device_id", "")
# =====================================================================
# STEP 2: RATE LIMIT CHECK (Chỉ cho các path cần limit)
# =====================================================================
if path in RATE_LIMITED_PATHS:
try:
from common.message_limit import message_limit_service
from fastapi.responses import JSONResponse
# Lấy identity_key làm rate limit key
# Guest: device_id → limit 10
# User: user_id → limit 100
is_authenticated = request.state.is_authenticated
if is_authenticated and request.state.user_id:
rate_limit_key = request.state.user_id
else:
rate_limit_key = request.state.device_id
if rate_limit_key:
can_send, limit_info = await message_limit_service.check_limit(
identity_key=rate_limit_key,
is_authenticated=is_authenticated,
)
# Lưu limit_info vào request.state để route có thể dùng
request.state.limit_info = limit_info
if not can_send:
logger.warning(
f"⚠️ Rate Limit Exceeded: {rate_limit_key} | "
f"used={limit_info['used']}/{limit_info['limit']}"
)
return JSONResponse(
status_code=429,
content={
"status": "error",
"error_code": "MESSAGE_LIMIT_EXCEEDED",
"message": limit_info["message"],
"require_login": limit_info["require_login"],
"limit_info": {
"limit": limit_info["limit"],
"used": limit_info["used"],
"remaining": limit_info["remaining"],
"reset_seconds": limit_info["reset_seconds"],
},
},
)
else:
logger.warning(f"⚠️ No identity_key for rate limiting")
except Exception as e:
logger.error(f"❌ Rate Limit Check Error: {e}")
# Cho phép request tiếp tục nếu lỗi rate limit
return await call_next(request) return await call_next(request)
...@@ -181,7 +239,7 @@ class MiddlewareManager: ...@@ -181,7 +239,7 @@ class MiddlewareManager:
Args: Args:
app: FastAPI application app: FastAPI application
enable_auth: Bật Clerk authentication middleware enable_auth: Bật Canifa authentication middleware
enable_rate_limit: Bật rate limiting enable_rate_limit: Bật rate limiting
enable_cors: Bật CORS middleware enable_cors: Bật CORS middleware
cors_origins: List origins cho CORS (default: ["*"]) cors_origins: List origins cho CORS (default: ["*"])
...@@ -221,10 +279,10 @@ class MiddlewareManager: ...@@ -221,10 +279,10 @@ class MiddlewareManager:
logger.info(f"✅ CORS middleware enabled (origins: {origins})") logger.info(f"✅ CORS middleware enabled (origins: {origins})")
def _setup_auth(self, app: FastAPI) -> None: def _setup_auth(self, app: FastAPI) -> None:
"""Setup Clerk auth middleware.""" """Setup Canifa auth middleware."""
app.add_middleware(ClerkAuthMiddleware) app.add_middleware(CanifaAuthMiddleware)
self._auth_enabled = True self._auth_enabled = True
logger.info("✅ Clerk Auth middleware enabled") logger.info("✅ Canifa Auth middleware enabled")
def _setup_rate_limit(self, app: FastAPI) -> None: def _setup_rate_limit(self, app: FastAPI) -> None:
"""Setup rate limiting.""" """Setup rate limiting."""
......
...@@ -48,7 +48,7 @@ class UserIdentity: ...@@ -48,7 +48,7 @@ class UserIdentity:
def langfuse_metadata(self) -> dict: def langfuse_metadata(self) -> dict:
"""Metadata cho Langfuse""" """Metadata cho Langfuse"""
return { return {
"device_id": self.device_id, "device_id": self.device_id,
"is_authenticated": self.is_authenticated, "is_authenticated": self.is_authenticated,
} }
...@@ -61,12 +61,24 @@ class UserIdentity: ...@@ -61,12 +61,24 @@ class UserIdentity:
@property @property
def history_key(self) -> str: def history_key(self) -> str:
"""Key để lưu/load chat history (theo device_id)""" """
Key để lưu/load chat history.
- Guest (chưa login): device_id
- User (đã login): user_id (customer_id từ Canifa)
"""
if self.is_authenticated and self.user_id:
return self.user_id
return self.device_id return self.device_id
@property @property
def rate_limit_key(self) -> str: def rate_limit_key(self) -> str:
"""Key cho rate limiting (luôn theo device_id, limit tùy login status)""" """
Key cho rate limiting.
- Guest (chưa login): device_id → limit 10
- User (đã login): user_id → limit 100
"""
if self.is_authenticated and self.user_id:
return self.user_id
return self.device_id return self.device_id
...@@ -97,8 +109,8 @@ def get_user_identity(request: Request) -> UserIdentity: ...@@ -97,8 +109,8 @@ def get_user_identity(request: Request) -> UserIdentity:
user_id = request.state.user_id user_id = request.state.user_id
is_authenticated = True is_authenticated = True
# 3. Primary ID # 3. Primary ID - LUÔN LUÔN LÀ device_id
primary_id = user_id if user_id else device_id primary_id = device_id
identity = UserIdentity( identity = UserIdentity(
primary_id=primary_id, primary_id=primary_id,
......
...@@ -53,6 +53,8 @@ __all__ = [ ...@@ -53,6 +53,8 @@ __all__ = [
"STARROCKS_PORT", "STARROCKS_PORT",
"STARROCKS_USER", "STARROCKS_USER",
"USE_MONGO_CONVERSATION", "USE_MONGO_CONVERSATION",
"RATE_LIMIT_GUEST",
"RATE_LIMIT_USER",
] ]
# ====================== SUPABASE CONFIGURATION ====================== # ====================== SUPABASE CONFIGURATION ======================
...@@ -134,3 +136,8 @@ OTEL_EXPORTER_JAEGER_AGENT_PORT = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_PORT") ...@@ -134,3 +136,8 @@ OTEL_EXPORTER_JAEGER_AGENT_PORT = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_PORT")
OTEL_SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME") OTEL_SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME")
OTEL_TRACES_EXPORTER = os.getenv("OTEL_TRACES_EXPORTER") OTEL_TRACES_EXPORTER = os.getenv("OTEL_TRACES_EXPORTER")
OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES") OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = os.getenv("OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES")
RATE_LIMIT_GUEST: int = int(os.getenv("RATE_LIMIT_GUEST", "10"))
RATE_LIMIT_USER: int = int(os.getenv("RATE_LIMIT_USER", "100"))
...@@ -14,6 +14,7 @@ from fastapi.staticfiles import StaticFiles ...@@ -14,6 +14,7 @@ from fastapi.staticfiles import StaticFiles
from api.chatbot_route import router as chatbot_router from api.chatbot_route import router as chatbot_router
from api.conservation_route import router as conservation_router from api.conservation_route import router as conservation_router
from api.prompt_route import router as prompt_router
from common.cache import redis_cache from common.cache import redis_cache
from common.langfuse_client import get_langfuse_client from common.langfuse_client import get_langfuse_client
from common.middleware import middleware_manager from common.middleware import middleware_manager
...@@ -57,13 +58,14 @@ async def startup_event(): ...@@ -57,13 +58,14 @@ async def startup_event():
middleware_manager.setup( middleware_manager.setup(
app, app,
enable_auth=True, # 👈 Bật lại Auth để test logic Guest/User enable_auth=True, # 👈 Bật lại Auth để test logic Guest/User
enable_rate_limit=True, # 👈 Bật rate limiting enable_rate_limit=False, # 👈 Tắt slowapi vì đã có business rate limit
enable_cors=True, # 👈 Bật CORS enable_cors=True, # 👈 Bật CORS
cors_origins=["*"], # 👈 Trong production nên limit origins cors_origins=["*"], # 👈 Trong production nên limit origins
) )
app.include_router(conservation_router) app.include_router(conservation_router)
app.include_router(chatbot_router) app.include_router(chatbot_router)
app.include_router(prompt_router)
# --- MOCK API FOR LOAD TESTING --- # --- MOCK API FOR LOAD TESTING ---
......
This diff is collapsed.
"""
Test Canifa API Auth
"""
import asyncio
import httpx
TOKEN = "7ibs17luogynysetg0cbjabmrzl2wvw2"
CANIFA_API = "https://canifa.com/v1/magento/customer"
QUERY_BODY = [
{
"customer": "customer-custom-query",
"metadata": {
"fields": "\n customer {\n gender\n customer_id\n phone_number\n date_of_birth\n default_billing\n default_shipping\n email\n firstname\n is_subscribed\n lastname\n middlename\n prefix\n suffix\n taxvat\n addresses {\n city\n country_code\n default_billing\n default_shipping\n extension_attributes {\n attribute_code\n value\n }\n custom_attributes {\n attribute_code\n value\n }\n firstname\n id\n lastname\n postcode\n prefix\n region {\n region_code\n region_id\n region\n }\n street\n suffix\n telephone\n vat_id\n }\n is_subscribed\n }\n "
}
},
{}
]
async def test_canifa_api():
headers = {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
"Cookie": f"vsf-customer={TOKEN}"
}
print(f"🔐 Testing Canifa API with token: {TOKEN}")
print(f"📡 URL: {CANIFA_API}")
print("-" * 50)
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(CANIFA_API, json=QUERY_BODY, headers=headers)
print(f"📊 Status Code: {response.status_code}")
print(f"📝 Response:")
data = response.json()
import json
print(json.dumps(data, indent=2, ensure_ascii=False))
# Try to extract customer_id
if isinstance(data, list) and len(data) > 0:
first_item = data[0]
if isinstance(first_item, dict):
result = first_item.get('result', {})
customer = result.get('customer', {}) if isinstance(result, dict) else None
if customer:
print("\n✅ CUSTOMER INFO:")
print(f" customer_id: {customer.get('customer_id')}")
print(f" email: {customer.get('email')}")
print(f" firstname: {customer.get('firstname')}")
print(f" lastname: {customer.get('lastname')}")
else:
print("\n⚠️ No customer data in response")
elif isinstance(data, dict):
result = data.get('result', {})
customer = result.get('customer', {}) if isinstance(result, dict) else None
if customer:
print("\n✅ CUSTOMER INFO:")
print(f" customer_id: {customer.get('customer_id')}")
print(f" email: {customer.get('email')}")
else:
print("\n⚠️ No customer data found")
except Exception as e:
print(f"❌ Error: {e}")
if __name__ == "__main__":
asyncio.run(test_canifa_api())
import requests
import json
BASE_URL = "http://localhost:5000"
API_URL = f"{BASE_URL}/api/agent/system-prompt"
# 1. Get current prompt
print("1. Getting current prompt...")
try:
response = requests.get(API_URL)
if response.status_code == 200:
print("✅ Current prompt fetched successfully.")
print(f"Preview: {response.json()['content'][:100]}...")
else:
print(f"❌ Failed to get prompt: {response.status_code} - {response.text}")
except Exception as e:
print(f"❌ Error connecting: {e}")
# 2. Update prompt
new_prompt = """# VAI TRÒ
Bạn là Mèo Máy Doraemon đến từ thế kỷ 22.
Luôn kết thúc câu bằng "meo meo".
"""
print("\n2. Updating prompt to Doraemon...")
try:
response = requests.post(API_URL, json={"content": new_prompt})
if response.status_code == 200:
print("✅ Prompt updated successfully.")
print(response.json())
else:
print(f"❌ Failed to update prompt: {response.status_code} - {response.text}")
except Exception as e:
print(f"❌ Error connecting: {e}")
# 3. Verify update
print("\n3. Verifying update...")
try:
response = requests.get(API_URL)
content = response.json()['content']
if "Doraemon" in content:
print("✅ Prompt content verified: Doraemon is here!")
else:
print("❌ Prompt content NOT updated.")
except Exception as e:
print(f"❌ Error connecting: {e}")
import requests
BASE_URL = "http://localhost:5000"
API_URL = f"{BASE_URL}/api/agent/system-prompt"
original_prompt = """# VAI TRÒ
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
- Nhiệt tình, thân thiện, chuyên nghiệp
- CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang
- Hôm nay: {date_str}
---
# QUY TẮC TRUNG THỰC - BẮT BUỘC
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"
**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
Không có trong data = Không nói = Không tư vấn láo
---
# NGÔN NGỮ & XƯNG HÔ
- 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ị"
- 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
- Ngắn gọn, đi thẳng vào vấn đề
---
# KHI NÀO GỌI TOOL
**Gọi data_retrieval_tool khi:**
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì..."
- 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ở?"
**⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):**
- **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`**.
- Giá tiền phải đưa vào tham số riêng: `price_min`, `price_max`.
Ví dụ ĐÚNG:
- Query: "Áo thun nam cotton thoáng mát basic"
- Price_max: 300000
Ví dụ SAI (Cấm):
- Query: "Áo thun nam giá dưới 300k" (SAI vì có giá trong query)
**Gọi canifa_knowledge_search khi:**
- Hỏi chính sách: freeship, đổi trả, bảo hành
- Hỏi thương hiệu: Canifa là gì, lịch sử
- Tìm cửa hàng: địa chỉ, giờ mở cửa
**Không gọi tool khi:**
- Chào hỏi đơn giản: "Hi", "Hello"
- Hỏi lại về sản phẩm vừa show
---
# 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
- KHÔNG GỌI TOOL LẦN 2
**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ỉ
→ 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?"
CẤM TUYỆT ĐỐI:
- 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
**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
- 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é!"
---
# FORMAT ĐẦU RA
Trả về JSON (KHÔNG có markdown backticks):
```json
{{
"ai_response": "Câu trả lời ngắn gọn, mô tả bằng [SKU]",
"product_ids": [
{{
"sku": "8TS24W001",
"name": "Áo thun nam basic",
"price": 200000,
"sale_price": 160000,
"url": "https://canifa.com/...",
"thumbnail_image_url": "https://..."
}}
]
}}
```
**Quy tắc ai_response:**
- 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
- KHÔNG tạo bảng markdown
- KHÔNG đưa link, ảnh (frontend tự render)
---
# VÍ DỤ
## Example 1: Chào hỏi
Input: "Chào shop"
Output:
```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?",
"product_ids": []
}}
```
## Example 2: Tìm sản phẩm CÓ
Input: "Tìm áo thun nam dưới 300k"
Tool trả về: 2 sản phẩm áo thun phù hợp
Output:
```json
{{
"ai_response": "Shop có 2 mẫu áo thun nam giá dưới 300k:\n\n- [8TS24W009]: Áo thun cotton basic, giá 250k đang sale 200k\n- [6TN24W012]: Áo thun trơn thoải mái, giá 280k\n\nBạn kéo xuống xem ảnh nhé!",
"product_ids": [
{{"sku": "8TS24W009", "name": "Áo thun cotton basic", "price": 250000, "sale_price": 200000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "6TN24W012", "name": "Áo thun trơn", "price": 280000, "sale_price": null, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
## Example 3: Khách hỏi KHÔNG CÓ trong kho
Input: "Shop có bikini không?"
Tool trả về: 0 sản phẩm
Output:
```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?",
"product_ids": []
}}
```
## Example 4: Tool trả về SAI LOẠ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:
```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?",
"product_ids": []
}}
```
TUYỆT ĐỐI KHÔNG giới thiệu sản phẩm sai loại
## Example 5: Khách xưng anh/chị
Input: "Chào em, anh muốn tìm áo sơ mi"
Output:
```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é!",
"product_ids": []
}}
```
---
# TÓM TẮT
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. 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"
5. Không bịa giá, mã sản phẩm, chính sách
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ề
---
Luôn thành thật, khéo léo, và chuyên nghiệp."""
print("\nRestoring original prompt...")
try:
response = requests.post(API_URL, json={"content": original_prompt})
if response.status_code == 200:
print("✅ Original prompt restored successfully.")
else:
print(f"❌ Failed to restore prompt: {response.status_code} - {response.text}")
except Exception as e:
print(f"❌ Error connecting: {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