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
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
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt")
def get_system_prompt() -> str:
"""
System prompt cho CiCi Fashion Agent
Đọc từ file system_prompt.txt để có thể update dynamic.
Returns:
str: System prompt với ngày hiện tại
......@@ -17,202 +20,18 @@ def get_system_prompt() -> str:
now = datetime.now()
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.
- 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}
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."""
return prompt.replace("{date_str}", date_str)
\ No newline at end of file
KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN.
"""
\ 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
FastAPI endpoints cho Fashion Q&A Agent service.
Router chỉ chứa định nghĩa API, logic nằm ở controller.
Message Limit:
- Guest (không login): 10 tin/ngày theo device_id
- User đã login: 100 tin/ngày theo user_id
Note: Rate limit check đã được xử lý trong middleware (CanifaAuthMiddleware)
"""
import logging
......@@ -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.
Message Limit:
- Guest: 10 tin nhắn/ngày (theo device_id)
- User đã login: 100 tin nhắn/ngày (theo user_id)
Note: Rate limit đã được check trong middleware.
"""
# 1. Xác định user identity
identity = get_user_identity(request)
user_id = identity.primary_id
# 2. Check message limit TRƯỚC khi xử lý
can_send, limit_info = await message_limit_service.check_limit(
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"],
},
}
# Rate limit đã check trong middleware, lấy limit_info từ request.state
limit_info = getattr(request.state, 'limit_info', None)
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:
background_tasks=background_tasks,
model_name=DEFAULT_MODEL,
images=req.images,
identity_key=identity.history_key, # Guest: device_id, User: user_id
)
# Log chi tiết response
......@@ -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(
identity_key=identity.rate_limit_key,
is_authenticated=identity.is_authenticated,
......
"""
Chat History API Routes
- GET /api/history/{user_id} - Lấy lịch sử chat (có product_ids)
- DELETE /api/history/{user_id} - Xóa lịch sử chat
- GET /api/history/{identity_key} - Lấy lịch sử chat (có product_ids)
- 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
......@@ -26,10 +28,14 @@ class ClearHistoryResponse(BaseModel):
message: str
@router.get("/api/history/{user_id}", summary="Get Chat History", response_model=ChatHistoryResponse)
async def get_chat_history(user_id: str, limit: int | None = 50, before_id: int | None = None):
@router.get("/api/history/{identity_key}", summary="Get Chat History", response_model=ChatHistoryResponse)
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:
- 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
"""
try:
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
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
return {"data": history, "next_cursor": next_cursor}
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")
@router.delete("/api/history/{user_id}", summary="Clear Chat History", response_model=ClearHistoryResponse)
async def clear_chat_history(user_id: str):
@router.delete("/api/history/{identity_key}", summary="Clear Chat History", response_model=ClearHistoryResponse)
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:
manager = await get_conversation_manager()
await manager.clear_history(user_id)
logger.info(f"✅ Cleared chat history for user {user_id}")
return {"success": True, "message": f"Đã xóa lịch sử chat của user {user_id}"}
await manager.clear_history(identity_key)
logger.info(f"✅ Cleared chat history for {identity_key}")
return {"success": True, "message": f"Đã xóa lịch sử chat của {identity_key}"}
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")
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]]:
if response.status_code == 200:
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 data.get("code") != 200:
logger.warning(f"Canifa API Business Error: {data.get('code')} - {data.get('result')}")
return None
return data.get("result", {})
# Trả về toàn bộ data để extract_user_id xử lý
return data
# Nếu Canifa trả list (đôi khi batch request trả về list)
# Nếu Canifa trả list (batch request)
return data
else:
......
This diff is collapsed.
......@@ -88,6 +88,7 @@ class LLMFactory:
"streaming": streaming,
"api_key": key,
"temperature": 0,
"max_tokens": 1000,
}
# Nếu bật json_mode, tiêm trực tiếp vào constructor
......
"""
Message Limit Service
Giới hạn số tin nhắn theo ngày:
- Guest (không login): 10 tin/ngày theo device_id
- User đã login: 100 tin/ngày theo user_id
- Guest (không login): RATE_LIMIT_GUEST tin/ngày theo device_id
- User đã login: RATE_LIMIT_USER tin/ngày theo user_id
Lưu trữ: Redis (dùng chung với cache.py)
"""
from __future__ import annotations
import logging
import os
from datetime import datetime
from common.cache import redis_cache
from config import RATE_LIMIT_GUEST, RATE_LIMIT_USER
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
MESSAGE_COUNT_PREFIX = "msg_limit:"
......@@ -32,6 +29,10 @@ class MessageLimitService:
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.
Limits:
- Guest (device_id): RATE_LIMIT_GUEST (default: 10)
- User (user_id): RATE_LIMIT_USER (default: 100)
Usage:
from common.message_limit import message_limit_service
......@@ -60,18 +61,18 @@ class MessageLimitService:
if MessageLimitService._initialized:
return
# Fallback in-memory storage: { "device_id": {"guest": 0, "user": 0} }
self._memory_storage: dict[str, dict[str, int]] = {}
# Fallback in-memory storage: { "identity_key": count }
self._memory_storage: dict[str, int] = {}
self._memory_date: str = ""
# Limits
self.guest_limit = 3 # Test limit
self.total_limit = 5 # Test limit
# Limits from config
self.guest_limit = RATE_LIMIT_GUEST # Default: 10
self.user_limit = RATE_LIMIT_USER # Default: 100
MessageLimitService._initialized = True
logger.info(
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:
# 2. Logic Checking
can_send = True
limit_display = self.total_limit
limit_display = self.user_limit
message = ""
require_login = False
# Check Total Limit (Hard limit cho device)
if total_used >= self.total_limit:
# Check User Limit (Hard limit cho identity)
if total_used >= self.user_limit:
can_send = False
# Thông báo khi hết tổng quota (dù là user hay guest)
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:
# Guest dùng hết 100 tin (hiếm, vì guest bị chặn ở 10 rồi, trừ khi login rồi logout)
message = f"Thiết bị này đã đạt giới hạn {self.total_limit} tin nhắn hôm nay."
# 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.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)
elif not is_authenticated:
......@@ -222,19 +223,19 @@ class MessageLimitService:
require_login = True
message = (
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
# Nếu là guest: remaining = min(guest_remaining, total_remaining)
# Thực ra guest chỉ care guest_remaining vì guest < total
# Nếu là guest: remaining = min(guest_remaining, user_remaining)
# Thực ra guest chỉ care guest_remaining vì guest < user
if is_authenticated:
remaining = max(0, self.total_limit - total_used)
remaining = max(0, self.user_limit - total_used)
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)
total_remaining = max(0, self.total_limit - total_used)
remaining = min(guest_remaining, total_remaining)
user_remaining = max(0, self.user_limit - total_used)
remaining = min(guest_remaining, user_remaining)
info = {
"limit": limit_display,
......
......@@ -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:
1. Frontend gửi request với Authorization: Bearer <clerk_token>
2. Middleware verify token và extract user_id
3. Attach user info vào request.state.user và request.state.user_id
4. Routes lấy trực tiếp từ request.state (không cần Depends)
1. Frontend gửi request với Authorization: Bearer <canifa_token>
2. Middleware verify token với Canifa API → extract customer_id
3. Check message rate limit (Guest: 10, User: 100)
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):
......@@ -68,9 +74,12 @@ class ClerkAuthMiddleware(BaseHTTPMiddleware):
if any(path.startswith(prefix) for prefix in PUBLIC_PATH_PREFIXES):
return await call_next(request)
# ✅ Authentication Process
# =====================================================================
# STEP 1: AUTHENTICATION (Canifa API)
# =====================================================================
try:
auth_header = request.headers.get("Authorization")
device_id = request.headers.get("device_id", "")
# ========== DEV MODE: Bypass auth ==========
dev_user_id = request.headers.get("X-Dev-User-Id")
......@@ -79,56 +88,105 @@ class ClerkAuthMiddleware(BaseHTTPMiddleware):
request.state.user = {"customer_id": dev_user_id}
request.state.user_id = dev_user_id
request.state.is_authenticated = True
request.state.device_id = device_id or dev_user_id
return await call_next(request)
# --- TRƯỜNG HỢP 1: KHÔNG CÓ TOKEN -> GUEST ---
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_id = None
request.state.is_authenticated = False
return await call_next(request)
# --- TRƯỜNG HỢP 2: CÓ TOKEN -> GỌI CANIFA VERIFY ---
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)
request.state.device_id = device_id
else:
# --- TRƯỜNG HỢP 2: CÓ TOKEN -> GỌI CANIFA VERIFY ---
token = auth_header.replace("Bearer ", "")
# 2. Lấy User ID
user_id = await extract_user_id_from_canifa_response(user_data)
if user_id:
# ✅ VERIFY THÀNH CÔNG -> USER VIP
request.state.user = user_data
request.state.user_id = user_id
request.state.token = token
request.state.is_authenticated = True
logger.debug(f"✅ Auth Success: User {user_id}")
else:
# ❌ VERIFY FAILED -> GUEST
logger.warning(f"⚠️ Invalid Canifa Token (No ID found) -> Guest Mode")
from common.canifa_api import verify_canifa_token, extract_user_id_from_canifa_response
try:
user_data = await verify_canifa_token(token)
user_id = await extract_user_id_from_canifa_response(user_data)
if user_id:
request.state.user = user_data
request.state.user_id = user_id
request.state.token = token
request.state.is_authenticated = True
request.state.device_id = device_id
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_id = None
request.state.is_authenticated = False
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
request.state.device_id = device_id
except Exception as e:
logger.error(f"❌ Middleware Unexpected Error: {e}")
# Fallback an toàn: Guest mode
logger.error(f"❌ Middleware Auth Error: {e}")
request.state.user = None
request.state.user_id = None
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)
......@@ -181,7 +239,7 @@ class MiddlewareManager:
Args:
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_cors: Bật CORS middleware
cors_origins: List origins cho CORS (default: ["*"])
......@@ -221,10 +279,10 @@ class MiddlewareManager:
logger.info(f"✅ CORS middleware enabled (origins: {origins})")
def _setup_auth(self, app: FastAPI) -> None:
"""Setup Clerk auth middleware."""
app.add_middleware(ClerkAuthMiddleware)
"""Setup Canifa auth middleware."""
app.add_middleware(CanifaAuthMiddleware)
self._auth_enabled = True
logger.info("✅ Clerk Auth middleware enabled")
logger.info("✅ Canifa Auth middleware enabled")
def _setup_rate_limit(self, app: FastAPI) -> None:
"""Setup rate limiting."""
......
......@@ -48,7 +48,7 @@ class UserIdentity:
def langfuse_metadata(self) -> dict:
"""Metadata cho Langfuse"""
return {
"device_id": self.device_id,
"device_id": self.device_id,
"is_authenticated": self.is_authenticated,
}
......@@ -61,12 +61,24 @@ class UserIdentity:
@property
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
@property
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
......@@ -97,8 +109,8 @@ def get_user_identity(request: Request) -> UserIdentity:
user_id = request.state.user_id
is_authenticated = True
# 3. Primary ID
primary_id = user_id if user_id else device_id
# 3. Primary ID - LUÔN LUÔN LÀ device_id
primary_id = device_id
identity = UserIdentity(
primary_id=primary_id,
......
......@@ -53,6 +53,8 @@ __all__ = [
"STARROCKS_PORT",
"STARROCKS_USER",
"USE_MONGO_CONVERSATION",
"RATE_LIMIT_GUEST",
"RATE_LIMIT_USER",
]
# ====================== SUPABASE CONFIGURATION ======================
......@@ -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_TRACES_EXPORTER = os.getenv("OTEL_TRACES_EXPORTER")
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
from api.chatbot_route import router as chatbot_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.langfuse_client import get_langfuse_client
from common.middleware import middleware_manager
......@@ -57,13 +58,14 @@ async def startup_event():
middleware_manager.setup(
app,
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
cors_origins=["*"], # 👈 Trong production nên limit origins
)
app.include_router(conservation_router)
app.include_router(chatbot_router)
app.include_router(prompt_router)
# --- 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