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

Update server config, docker-compose and add test scripts

parent 79dfe7d5
......@@ -7,22 +7,13 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
---
# 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:
**ĐÚ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
Tool trả về áo thun → Giới thiệu áo thun
Tool trả về 0 sản phẩm → Nói "Shop chưa có sản phẩm này"
Tool trả về quần nỉ mà khách hỏi bikini → Nói "Shop chưa có bikini"
Khách hỏi giá online vs offline mà không có data → "Mình không rõ chi tiết so sánh giá, bạn có thể xem trực tiếp trên web hoặc liên hệ hotline nhé"
---
# NGÔN NGỮ & XƯNG HÔ
......
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import os
import re
from agent.graph import reset_graph
router = APIRouter()
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "../agent/system_prompt.txt")
# Allowed variables in prompt (single braces OK for these)
ALLOWED_VARIABLES = {"date_str"}
class PromptUpdateRequest(BaseModel):
content: str
def validate_prompt_braces(content: str) -> tuple[bool, list[str]]:
"""
Validate that all braces in prompt are properly escaped.
Returns (is_valid, list of problematic patterns)
"""
# Find all {word} patterns
single_brace_pattern = re.findall(r'\{([^{}]+)\}', content)
# Filter out allowed variables
problematic = [
var for var in single_brace_pattern
if var.strip() not in ALLOWED_VARIABLES and not var.startswith('{')
]
return len(problematic) == 0, problematic
@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:
......@@ -28,6 +47,19 @@ async def get_system_prompt_content():
async def update_system_prompt_content(request: PromptUpdateRequest):
"""Update system prompt content"""
try:
# Validate braces
is_valid, problematic = validate_prompt_braces(request.content)
if not is_valid:
# Return warning but still allow save
warning = (
f"⚠️ Phát hiện {{...}} chưa escape: {problematic[:3]}... "
f"Nếu đây là JSON, hãy dùng {{{{ }}}} thay vì {{ }}. "
f"Prompt vẫn được lưu nhưng có thể gây lỗi khi chat."
)
else:
warning = None
# 1. Update file
with open(PROMPT_FILE_PATH, "w", encoding="utf-8") as f:
f.write(request.content)
......@@ -35,6 +67,14 @@ async def update_system_prompt_content(request: PromptUpdateRequest):
# 2. Reset Graph Singleton to force reload prompt
reset_graph()
return {"status": "success", "message": "System prompt updated successfully. Graph reloaded."}
response = {
"status": "success",
"message": "System prompt updated successfully. Graph reloaded."
}
if warning:
response["warning"] = warning
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
......@@ -4,6 +4,7 @@ Singleton Pattern cho cả 2 services
"""
from __future__ import annotations
import json
import logging
from collections.abc import Callable
from typing import TYPE_CHECKING
......@@ -79,7 +80,25 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
# =====================================================================
try:
auth_header = request.headers.get("Authorization")
device_id = request.headers.get("device_id", "")
# --- Device ID from Body ---
device_id = ""
if method in ["POST", "PUT", "PATCH"]:
try:
body_bytes = await request.body()
async def receive_wrapper():
return {"type": "http.request", "body": body_bytes}
request._receive = receive_wrapper
if body_bytes:
try:
body_json = json.loads(body_bytes)
device_id = body_json.get("device_id", "")
except json.JSONDecodeError:
pass
except Exception as e:
logger.warning(f"Error reading device_id from body: {e}")
# ========== DEV MODE: Bypass auth ==========
dev_user_id = request.headers.get("X-Dev-User-Id")
......@@ -133,7 +152,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
request.state.user = None
request.state.user_id = None
request.state.is_authenticated = False
request.state.device_id = request.headers.get("device_id", "")
request.state.device_id = ""
# =====================================================================
# STEP 2: RATE LIMIT CHECK (Chỉ cho các path cần limit)
......
......@@ -15,8 +15,18 @@ services:
resources:
limits:
memory: 8g
networks:
- backend_network
logging:
driver: "json-file"
options:
tag: "{{.Name}}"
networks:
backend_network:
driver: bridge
ipam:
driver: default
config:
- subnet: "172.24.0.0/16"
gateway: "172.24.0.1"
import requests
import json
import time
url = "http://localhost:5000/api/agent/chat"
token = "071w198x23ict4hs1i6bl889fit5p3f7"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
payload = {
"user_query": "tư vấn cho mình áo hoodie"
}
print(f"Sending AUTHENTICATED POST request to {url}...")
print(f"Token: {token}")
start = time.time()
try:
response = requests.post(url, json=payload, headers=headers, timeout=120)
print(f"Status Code: {response.status_code}")
print(f"Time taken: {time.time() - start:.2f}s")
if response.status_code == 200:
data = response.json()
print("Response JSON:")
# Print limit info specifically to check if limit increased to USER level (100)
if "limit_info" in data:
print("Limit Info:", json.dumps(data["limit_info"], indent=2))
else:
print(json.dumps(data, indent=2, ensure_ascii=False))
else:
print("Error Response:")
print(response.text)
except Exception as e:
print(f"Error: {e}")
import httpx
import asyncio
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def test_auth():
url = "http://localhost:5000/api/agent/chat"
# 1. Test GUEST Mode (No Token)
logger.info("--- TEST 1: GUEST MODE (No Token) ---")
payload_guest = {
"device_id": "device_guest_123",
"user_query": "hello guest"
}
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(url, json=payload_guest)
if resp.status_code == 200:
data = resp.json()
limit = data.get("limit_info", {}).get("limit")
used = data.get("limit_info", {}).get("used")
logger.info(f"✅ Guest Response Limit: {limit} (Expected 10)")
if limit == 10:
logger.info("=> Logic Guest OK")
else:
logger.error(f"=> Logic Guest FAILED (Limit is {limit})")
else:
logger.error(f"Request failed: {resp.text}")
except Exception as e:
logger.error(f"Error Test 1: {e}")
# 2. Test USER Mode (With Token)
logger.info("\n--- TEST 2: USER MODE (With Access Token) ---")
token = "071w198x23ict4hs1i6bl889fit5p3f7"
payload_user = {
"device_id": "device_user_123", # device_id này sẽ bị ignore nếu token valid
"user_query": "hello user"
}
headers = {
"Authorization": f"Bearer {token}"
}
try:
async with httpx.AsyncClient(timeout=20.0) as client:
resp = await client.post(url, json=payload_user, headers=headers)
if resp.status_code == 200:
data = resp.json()
limit = data.get("limit_info", {}).get("limit")
used = data.get("limit_info", {}).get("used")
logger.info(f"✅ User Response Limit: {limit} (Expected 100)")
if limit == 100:
logger.info("=> Logic User OK (Prioritized Token over DeviceID)")
elif limit == 10:
logger.warning("=> Logic User FAILED (Still Guest Mode). Token might be invalid or Canifa API unreachable.")
else:
logger.info(f"=> Unexpected Limit: {limit}")
elif resp.status_code == 429:
logger.warning("Rate limit exceeded for this user/device.")
else:
logger.error(f"Request failed: {resp.status_code} - {resp.text}")
except Exception as e:
logger.error(f"Error Test 2: {e}")
if __name__ == "__main__":
asyncio.run(test_auth())
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