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. ...@@ -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 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ề á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ề 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"
- 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é"
**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Ô # NGÔN NGỮ & XƯNG HÔ
......
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
import os import os
import re
from agent.graph import reset_graph from agent.graph import reset_graph
router = APIRouter() router = APIRouter()
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "../agent/system_prompt.txt") 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): class PromptUpdateRequest(BaseModel):
content: str 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") @router.get("/api/agent/system-prompt")
async def get_system_prompt_content(): async def get_system_prompt_content():
"""Get current system prompt content""" """Get current system prompt content"""
# ... existing code ...
try: try:
if os.path.exists(PROMPT_FILE_PATH): if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f: with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
...@@ -28,6 +47,19 @@ async def get_system_prompt_content(): ...@@ -28,6 +47,19 @@ async def get_system_prompt_content():
async def update_system_prompt_content(request: PromptUpdateRequest): async def update_system_prompt_content(request: PromptUpdateRequest):
"""Update system prompt content""" """Update system prompt content"""
try: 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 # 1. Update file
with open(PROMPT_FILE_PATH, "w", encoding="utf-8") as f: with open(PROMPT_FILE_PATH, "w", encoding="utf-8") as f:
f.write(request.content) f.write(request.content)
...@@ -35,6 +67,14 @@ async def update_system_prompt_content(request: PromptUpdateRequest): ...@@ -35,6 +67,14 @@ async def update_system_prompt_content(request: PromptUpdateRequest):
# 2. Reset Graph Singleton to force reload prompt # 2. Reset Graph Singleton to force reload prompt
reset_graph() 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: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
...@@ -4,6 +4,7 @@ Singleton Pattern cho cả 2 services ...@@ -4,6 +4,7 @@ Singleton Pattern cho cả 2 services
""" """
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
from collections.abc import Callable from collections.abc import Callable
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
...@@ -79,7 +80,25 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -79,7 +80,25 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
# ===================================================================== # =====================================================================
try: try:
auth_header = request.headers.get("Authorization") 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 MODE: Bypass auth ==========
dev_user_id = request.headers.get("X-Dev-User-Id") dev_user_id = request.headers.get("X-Dev-User-Id")
...@@ -133,7 +152,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -133,7 +152,7 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
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", "") request.state.device_id = ""
# ===================================================================== # =====================================================================
# STEP 2: RATE LIMIT CHECK (Chỉ cho các path cần limit) # STEP 2: RATE LIMIT CHECK (Chỉ cho các path cần limit)
......
...@@ -15,8 +15,18 @@ services: ...@@ -15,8 +15,18 @@ services:
resources: resources:
limits: limits:
memory: 8g memory: 8g
networks:
- backend_network
logging: logging:
driver: "json-file" driver: "json-file"
options: options:
tag: "{{.Name}}" 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