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

Update project state

parent d21e9eec
# Python # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
*.so *.so
.Python .Python
env/ env/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
# Virtual Environment # Virtual Environment
.venv/ .venv/
venv/ venv/
ENV/ ENV/
# IDEs # IDEs
.vscode/ .vscode/
.idea/ .idea/
# Backend specifically # Backend specifically
backend/.env backend/.env
backend/.venv/ backend/.venv/
backend/__pycache__/ backend/__pycache__/
backend/*.pyc backend/*.pyc
# Preference folder (development/temporary) # Preference folder (development/temporary)
preference/ preference/
# Development/Test folders # Development/Test folders
backend/hehe/ backend/hehe/
backend/test/ backend/test/
backend/scripts/ backend/scripts/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Misc # Misc
.ruff_cache/ .ruff_cache/
*.log *.log
!backend/requirements.txt !backend/requirements.txt
run.txt run.txt
stages: stages:
- deploy - deploy
deploy_to_server: deploy_to_server:
stage: deploy stage: deploy
# Chế độ shell chạy trực tiếp trên host # Chế độ shell chạy trực tiếp trên host
script: script:
- echo "🚀 Bắt đầu quá trình Deploy Sạch..." - echo "🚀 Bắt đầu quá trình Deploy Sạch..."
- export DOCKER_BUILDKIT=1 - export DOCKER_BUILDKIT=1
- "if [ -f /home/anhvh/canifa_soure/chatbot-canifa/backend/.env ]; then cp /home/anhvh/canifa_soure/chatbot-canifa/backend/.env backend/.env; fi" - "if [ -f /home/anhvh/canifa_soure/chatbot-canifa/backend/.env ]; then cp /home/anhvh/canifa_soure/chatbot-canifa/backend/.env backend/.env; fi"
- cd backend - cd backend
- docker stop canifa_backend || true - docker stop canifa_backend || true
- docker rm -f canifa_backend || true - docker rm -f canifa_backend || true
- docker compose -p canifa-chatbot down --remove-orphans || true - docker compose -p canifa-chatbot down --remove-orphans || true
- docker compose -p canifa-chatbot up --build -d - docker compose -p canifa-chatbot up --build -d
- docker system prune -f - docker system prune -f
- echo "✅ Web ĐÃ CẬP NHẬT THÀNH CÔNG!" - echo "✅ Web ĐÃ CẬP NHẬT THÀNH CÔNG!"
only: only:
- master - master
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.8.4 rev: v0.8.4
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
args: [ --fix ] args: [ --fix ]
# Run the formatter. # Run the formatter.
- id: ruff-format - id: ruff-format
__pycache__ __pycache__
*.pyc *.pyc
.env .env
.venv .venv
venv venv
.git .git
.gitignore .gitignore
.dockerignore .dockerignore
logs logs
data data
# EditorConfig - đảm bảo indentation nhất quán # EditorConfig - đảm bảo indentation nhất quán
# https://editorconfig.org # https://editorconfig.org
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.py] [*.py]
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
max_line_length = 120 max_line_length = 120
[*.{json,yml,yaml}] [*.{json,yml,yaml}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
# Ignore embedded repo # Ignore embedded repo
preference/ preference/
Requirement already satisfied: python-socketio in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (5.13.0) Requirement already satisfied: python-socketio in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (5.13.0)
Requirement already satisfied: bidict>=0.21.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-socketio) (0.23.1) Requirement already satisfied: bidict>=0.21.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-socketio) (0.23.1)
Requirement already satisfied: python-engineio>=4.11.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-socketio) (4.12.2) Requirement already satisfied: python-engineio>=4.11.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-socketio) (4.12.2)
Requirement already satisfied: simple-websocket>=0.10.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-engineio>=4.11.0->python-socketio) (1.1.0) Requirement already satisfied: simple-websocket>=0.10.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from python-engineio>=4.11.0->python-socketio) (1.1.0)
Requirement already satisfied: wsproto in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from simple-websocket>=0.10.0->python-engineio>=4.11.0->python-socketio) (1.2.0) Requirement already satisfied: wsproto in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from simple-websocket>=0.10.0->python-engineio>=4.11.0->python-socketio) (1.2.0)
Requirement already satisfied: h11<1,>=0.9.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from wsproto->simple-websocket>=0.10.0->python-engineio>=4.11.0->python-socketio) (0.16.0) Requirement already satisfied: h11<1,>=0.9.0 in c:\users\fptshop\miniconda3\envs\robot\lib\site-packages (from wsproto->simple-websocket>=0.10.0->python-engineio>=4.11.0->python-socketio) (0.16.0)
# Canifa Chatbot API (Simplified) # Canifa Chatbot API (Simplified)
Base URL: `http://172.16.2.207:5000` Base URL: `http://172.16.2.207:5000`
--- ---
## 1. Chat (Gửi tin nhắn) ## 1. Chat (Gửi tin nhắn)
**POST** `/api/agent/chat` **POST** `/api/agent/chat`
### Request ### Request
#### Guest (Chưa login) #### Guest (Chưa login)
```json ```json
{ {
"user_query": "Tìm áo thun nam", "user_query": "Tìm áo thun nam",
"device_id": "my-device-123" "device_id": "my-device-123"
} }
``` ```
#### User (Đã login) #### User (Đã login)
```json ```json
Headers: Authorization: Bearer <token> Headers: Authorization: Bearer <token>
{ {
"user_query": "Tìm áo thun nam", "user_query": "Tìm áo thun nam",
"device_id": "my-device-123" "device_id": "my-device-123"
} }
``` ```
### Response ### Response
```json ```json
{ {
"status": "success", "status": "success",
"ai_response": "Shop có mẫu áo thun này...", "ai_response": "Shop có mẫu áo thun này...",
"product_ids": [ "product_ids": [
{ {
"sku": "8TS24W001", "sku": "8TS24W001",
"name": "Áo thun nam Basic", "name": "Áo thun nam Basic",
"price": 250000, "price": 250000,
"sale_price": 199000, "sale_price": 199000,
"url": "https://canifa.com/...", "url": "https://canifa.com/...",
"thumbnail_image_url": "https://..." "thumbnail_image_url": "https://..."
} }
], ],
"limit_info": { "limit": 10, "used": 1, "remaining": 9 } "limit_info": { "limit": 10, "used": 1, "remaining": 9 }
} }
``` ```
### Error Response (500) ### Error Response (500)
Trong trường hợp lỗi hệ thống (DB, LLM...), API sẽ trả về HTTP 500 kèm body: Trong trường hợp lỗi hệ thống (DB, LLM...), API sẽ trả về HTTP 500 kèm body:
```json ```json
{ {
"status": "error", "status": "error",
"error_code": "SYSTEM_ERROR", "error_code": "SYSTEM_ERROR",
"message": "Oops 😥 Hiện Canifa-AI chưa thể xử lý yêu cầu của bạn..." "message": "Oops 😥 Hiện Canifa-AI chưa thể xử lý yêu cầu của bạn..."
} }
``` ```
### Error Response (429) - Rate Limit Exceeded ### Error Response (429) - Rate Limit Exceeded
Khi user/guest vượt quá giới hạn tin nhắn cho phép: Khi user/guest vượt quá giới hạn tin nhắn cho phép:
**Trường hợp 1: Guest hết lượt (Cần login)** **Trường hợp 1: Guest hết lượt (Cần login)**
```json ```json
{ {
"status": "error", "status": "error",
"error_code": "GUEST_LIMIT_EXCEEDED", "error_code": "GUEST_LIMIT_EXCEEDED",
"message": "Bạn đã sử dụng hết tin nhắn hôm nay. Đăng nhập ngay để dùng tiếp: https://canifa.com/login", "message": "Bạn đã sử dụng hết tin nhắn hôm nay. Đăng nhập ngay để dùng tiếp: https://canifa.com/login",
"require_login": true, "require_login": true,
"limit_info": { "limit_info": {
"limit": 10, "limit": 10,
"used": 10, "used": 10,
"remaining": 0, "remaining": 0,
"reset_seconds": 3600 "reset_seconds": 3600
} }
} }
``` ```
**Trường hợp 2: User hết lượt (Hoặc Guest đạt Hard Limit)** **Trường hợp 2: User hết lượt (Hoặc Guest đạt Hard Limit)**
```json ```json
{ {
"status": "error", "status": "error",
"error_code": "USER_LIMIT_EXCEEDED", "error_code": "USER_LIMIT_EXCEEDED",
"message": "Bạn đã sử dụng hết tin nhắn hôm nay. Vui lòng quay lại vào hôm sau để dùng tiếp!", "message": "Bạn đã sử dụng hết tin nhắn hôm nay. Vui lòng quay lại vào hôm sau để dùng tiếp!",
"require_login": false, "require_login": false,
"limit_info": { ... } "limit_info": { ... }
} }
``` ```
--- ---
## 2. History (Lấy lịch sử) ## 2. History (Lấy lịch sử)
**GET** `/api/history/{your_device_id}?limit=20&before_id=105` **GET** `/api/history/{your_device_id}?limit=20&before_id=105`
### Query Parameters ### Query Parameters
| Param | Type | Description | | Param | Type | Description |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| `limit` | int | Số tin nhắn (Default: 50) | | `limit` | int | Số tin nhắn (Default: 50) |
| `before_id` | int | ID tin nhắn cuối của trang trước (để load thêm) | | `before_id` | int | ID tin nhắn cuối của trang trước (để load thêm) |
### Request ### Request
**Guest:** **Guest:**
`/api/history/my-device-123?limit=20` `/api/history/my-device-123?limit=20`
**User:** **User:**
`/api/history/my-device-123?limit=20` (Param URL vẫn giữ là device_id cho tiện FE) `/api/history/my-device-123?limit=20` (Param URL vẫn giữ là device_id cho tiện FE)
Header: `Authorization: Bearer <token>` Header: `Authorization: Bearer <token>`
*(Backend sẽ tự ưu tiên lấy User ID từ Token để truy vấn lịch sử)* *(Backend sẽ tự ưu tiên lấy User ID từ Token để truy vấn lịch sử)*
### Response ### Response
```json ```json
{ {
"data": [ "data": [
{ {
"id": 105, "id": 105,
"message": "...", // JSON String "message": "...", // JSON String
"is_human": false, "is_human": false,
"timestamp": "..." "timestamp": "..."
} }
], ],
"next_cursor": 104 // Dùng ID này cho `before_id` tiếp theo "next_cursor": 104 // Dùng ID này cho `before_id` tiếp theo
} }
``` ```
--- ---
## 3. Reset (Xóa và tạo mới) ## 3. Reset (Xóa và tạo mới)
**POST** `/api/history/archive` **POST** `/api/history/archive`
*(Lưu ý: Chỉ dành cho User đã đăng nhập)* *(Lưu ý: Chỉ dành cho User đã đăng nhập)*
### Request ### Request
Header `Authorization` (User). Header `Authorization` (User).
Body rỗng `{}`. Body rỗng `{}`.
### Response ### Response
```json ```json
{ {
"status": "success", "status": "success",
"success": true, "success": true,
"message": "Archived successfully", "message": "Archived successfully",
"new_key": "my-device-123_archived_..." "new_key": "my-device-123_archived_..."
} }
``` ```
# ============================================================ # ============================================================
# DOCKERFILE.DEV - Local Development (Hot Reload + Cache) # DOCKERFILE.DEV - Local Development (Hot Reload + Cache)
# ============================================================ # ============================================================
# Sử dụng Python 3.11 Slim để tối ưu dung lượng # Sử dụng Python 3.11 Slim để tối ưu dung lượng
FROM python:3.11-slim FROM python:3.11-slim
# Thiết lập thư mục làm việc # Thiết lập thư mục làm việc
WORKDIR /app WORKDIR /app
# Thiết lập biến môi trường # Thiết lập biến môi trường
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV ENV=development ENV ENV=development
# Copy requirements.txt trước để tận dụng Docker cache # Copy requirements.txt trước để tận dụng Docker cache
COPY requirements.txt . COPY requirements.txt .
# Cài đặt thư viện Python (Docker layer cache) # Cài đặt thư viện Python (Docker layer cache)
RUN pip install -r requirements.txt && pip install watchdog[watchmedo] RUN pip install -r requirements.txt && pip install watchdog[watchmedo]
# Copy toàn bộ source code vào image # Copy toàn bộ source code vào image
COPY . . COPY . .
# Expose port 5000 # Expose port 5000
EXPOSE 5000 EXPOSE 5000
# Health check (optional) # Health check (optional)
HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=2 \ HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=2 \
CMD python -c "import requests; requests.get('http://localhost:5000/docs')" || exit 1 CMD python -c "import requests; requests.get('http://localhost:5000/docs')" || exit 1
CMD ["gunicorn", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:5000", "--timeout", "120", "--reload", "server:app"] CMD ["gunicorn", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:5000", "--timeout", "120", "--reload", "server:app"]
FROM python:3.11-slim FROM python:3.11-slim
WORKDIR /app WORKDIR /app
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV ENV=development ENV ENV=development
COPY requirements.txt . COPY requirements.txt .
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
COPY . . COPY . .
# Expose port 5000 (Port chạy server) # Expose port 5000 (Port chạy server)
EXPOSE 5000 EXPOSE 5000
# Tự động tính số worker = (Số Core * 2) + 1 để tận dụng tối đa CPU # Tự động tính số worker = (Số Core * 2) + 1 để tận dụng tối đa CPU
CMD gunicorn server:app --workers $(( 2 * $(nproc) + 1 )) --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:5000 --timeout 60 CMD gunicorn server:app --workers $(( 2 * $(nproc) + 1 )) --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:5000 --timeout 60
# Makefile cho CANIFA Chatbot # Makefile cho CANIFA Chatbot
.PHONY: up down restart logs build ps clean setup-nginx monitor-up monitor-down build-dev run-dev .PHONY: up down restart logs build ps clean setup-nginx monitor-up monitor-down build-dev run-dev
up: up:
sudo docker compose up -d --build sudo docker compose up -d --build
down: down:
docker-compose down docker-compose down
restart: restart:
docker-compose restart backend docker-compose restart backend
logs: logs:
sudo sudo
ps: ps:
docker-compose ps docker-compose ps
build: build:
docker-compose build docker-compose build
build-dev: build-dev:
docker build -f Dockerfile.dev -t canifa-backend:dev . docker build -f Dockerfile.dev -t canifa-backend:dev .
run-dev: run-dev:
docker run -it --rm -v $(PWD):/app -p 5000:5000 canifa-backend:dev docker run -it --rm -v $(PWD):/app -p 5000:5000 canifa-backend:dev
clean: clean:
docker-compose down -v --rmi all --remove-orphans docker-compose down -v --rmi all --remove-orphans
setup-nginx: setup-nginx:
@echo "🚀 Đang cấu hình Nginx..." @echo "🚀 Đang cấu hình Nginx..."
sudo cp nginx.conf /etc/nginx/sites-available/chatbot sudo cp nginx.conf /etc/nginx/sites-available/chatbot
sudo ln -sf /etc/nginx/sites-available/chatbot /etc/nginx/sites-enabled/ sudo ln -sf /etc/nginx/sites-available/chatbot /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl restart nginx sudo nginx -t && sudo systemctl restart nginx
@echo "✅ Nginx đã được cấu hình và restart!" @echo "✅ Nginx đã được cấu hình và restart!"
""" """
Backend Package Backend Package
""" """
""" """
Fashion Q&A Agent Package Fashion Q&A Agent Package
""" """
from .graph import build_graph from .graph import build_graph
from .models import AgentConfig, AgentState, get_config from .models import AgentConfig, AgentState, get_config
__all__ = [ __all__ = [
"AgentConfig", "AgentConfig",
"AgentState", "AgentState",
"build_graph", "build_graph",
"get_config", "get_config",
] ]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
from typing import Annotated, Any, TypedDict from typing import Annotated, Any, TypedDict
from langchain_core.messages import BaseMessage from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages from langgraph.graph.message import add_messages
from pydantic import BaseModel from pydantic import BaseModel
import config as global_config import config as global_config
class QueryRequest(BaseModel): class QueryRequest(BaseModel):
"""API Request model cho Fashion Q&A Chat""" """API Request model cho Fashion Q&A Chat"""
user_id: str | None = None user_id: str | None = None
user_query: str user_query: str
images: list[str] | None = None images: list[str] | None = None
image_analysis: dict[str, Any] | None = None image_analysis: dict[str, Any] | None = None
class AgentState(TypedDict): class AgentState(TypedDict):
"""Trạng thái của Agent trong LangGraph.""" """Trạng thái của Agent trong LangGraph."""
user_query: BaseMessage user_query: BaseMessage
history: list[BaseMessage] history: list[BaseMessage]
user_id: str | None user_id: str | None
ai_response: BaseMessage | None ai_response: BaseMessage | None
images_embedding: list[str] | None images_embedding: list[str] | None
messages: Annotated[list[BaseMessage], add_messages] messages: Annotated[list[BaseMessage], add_messages]
class AgentConfig: class AgentConfig:
"""Class chứa cấu hình runtime cho Agent.""" """Class chứa cấu hình runtime cho Agent."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.model_name = kwargs.get("model_name") or global_config.DEFAULT_MODEL self.model_name = kwargs.get("model_name") or global_config.DEFAULT_MODEL
self.openai_api_key = kwargs.get("openai_api_key") self.openai_api_key = kwargs.get("openai_api_key")
self.google_api_key = kwargs.get("google_api_key") self.google_api_key = kwargs.get("google_api_key")
self.groq_api_key = kwargs.get("groq_api_key") self.groq_api_key = kwargs.get("groq_api_key")
self.supabase_url = kwargs.get("supabase_url") self.supabase_url = kwargs.get("supabase_url")
self.supabase_key = kwargs.get("supabase_key") self.supabase_key = kwargs.get("supabase_key")
self.langfuse_public_key = kwargs.get("langfuse_public_key") self.langfuse_public_key = kwargs.get("langfuse_public_key")
self.langfuse_secret_key = kwargs.get("langfuse_secret_key") self.langfuse_secret_key = kwargs.get("langfuse_secret_key")
self.langfuse_base_url = kwargs.get("langfuse_base_url") self.langfuse_base_url = kwargs.get("langfuse_base_url")
def get_config() -> AgentConfig: def get_config() -> AgentConfig:
"""Khởi tạo cấu hình Agent từ các biến môi trường.""" """Khởi tạo cấu hình Agent từ các biến môi trường."""
return AgentConfig( return AgentConfig(
model_name=global_config.DEFAULT_MODEL, model_name=global_config.DEFAULT_MODEL,
openai_api_key=global_config.OPENAI_API_KEY, openai_api_key=global_config.OPENAI_API_KEY,
google_api_key=global_config.GOOGLE_API_KEY, google_api_key=global_config.GOOGLE_API_KEY,
groq_api_key=global_config.GROQ_API_KEY, groq_api_key=global_config.GROQ_API_KEY,
supabase_url=global_config.AI_SUPABASE_URL, supabase_url=global_config.AI_SUPABASE_URL,
supabase_key=global_config.AI_SUPABASE_KEY, supabase_key=global_config.AI_SUPABASE_KEY,
langfuse_public_key=global_config.LANGFUSE_PUBLIC_KEY, langfuse_public_key=global_config.LANGFUSE_PUBLIC_KEY,
langfuse_secret_key=global_config.LANGFUSE_SECRET_KEY, langfuse_secret_key=global_config.LANGFUSE_SECRET_KEY,
langfuse_base_url=global_config.LANGFUSE_BASE_URL, langfuse_base_url=global_config.LANGFUSE_BASE_URL,
) )
""" """
Agent Nodes Package Agent Nodes Package
""" """
from .agent import agent_node from .agent import agent_node
__all__ = ["agent_node"] __all__ = ["agent_node"]
""" """
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 3.0 - Dynamic from File Version 3.0 - Dynamic from File
""" """
import os import os
from datetime import datetime from datetime import datetime
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt") 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. Đọ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
""" """
now = datetime.now() now = datetime.now()
date_str = now.strftime("%d/%m/%Y") date_str = now.strftime("%d/%m/%Y")
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:
prompt_template = f.read() prompt_template = f.read()
return prompt_template.replace("{date_str}", date_str) return prompt_template.replace("{date_str}", date_str)
except Exception as e: except Exception as e:
print(f"Error reading system prompt file: {e}") print(f"Error reading system prompt file: {e}")
# Fallback default prompt if file error # Fallback default prompt if file error
return f"""# VAI TRÒ 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.
Hôm nay: {date_str} Hôm nay: {date_str}
KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN. KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN.
""" """
\ No newline at end of file
This diff is collapsed.
""" """
Tools Package Tools Package
Export tool và factory function Export tool và factory function
""" """
from .data_retrieval_tool import data_retrieval_tool from .data_retrieval_tool import data_retrieval_tool
from .get_tools import get_all_tools from .get_tools import get_all_tools
__all__ = ["data_retrieval_tool", "get_all_tools"] __all__ = ["data_retrieval_tool", "get_all_tools"]
import logging import logging
from langchain_core.tools import tool from langchain_core.tools import tool
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from common.embedding_service import create_embedding_async from common.embedding_service import create_embedding_async
from common.starrocks_connection import get_db_connection from common.starrocks_connection import get_db_connection
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class KnowledgeSearchInput(BaseModel): class KnowledgeSearchInput(BaseModel):
query: str = Field( query: str = Field(
description="Câu hỏi hoặc nhu cầu tìm kiếm thông tin phi sản phẩm của khách hàng (ví dụ: tìm cửa hàng, hỏi chính sách, tra bảng size...)" description="Câu hỏi hoặc nhu cầu tìm kiếm thông tin phi sản phẩm của khách hàng (ví dụ: tìm cửa hàng, hỏi chính sách, tra bảng size...)"
) )
@tool("canifa_knowledge_search", args_schema=KnowledgeSearchInput) @tool("canifa_knowledge_search", args_schema=KnowledgeSearchInput)
async def canifa_knowledge_search(query: str) -> str: async def canifa_knowledge_search(query: str) -> str:
""" """
Tra cứu TOÀN BỘ thông tin về thương hiệu và dịch vụ của Canifa. Tra cứu TOÀN BỘ thông tin về thương hiệu và dịch vụ của Canifa.
Sử dụng tool này khi khách hàng hỏi về: Sử dụng tool này khi khách hàng hỏi về:
1. THƯƠNG HIỆU & GIỚI THIỆU: Lịch sử hình thành, giá trị cốt lõi, sứ mệnh. 1. THƯƠNG HIỆU & GIỚI THIỆU: Lịch sử hình thành, giá trị cốt lõi, sứ mệnh.
2. HỆ THỐNG CỬA HÀNG: Tìm địa chỉ, số điện thoại, giờ mở cửa các cửa hàng tại các tỉnh thành (Hà Nội, HCM, Đà Nẵng, v.v.). 2. HỆ THỐNG CỬA HÀNG: Tìm địa chỉ, số điện thoại, giờ mở cửa các cửa hàng tại các tỉnh thành (Hà Nội, HCM, Đà Nẵng, v.v.).
3. CHÍNH SÁCH BÁN HÀNG: Quy định đổi trả, bảo hành, chính sách vận chuyển, phí ship. 3. CHÍNH SÁCH BÁN HÀNG: Quy định đổi trả, bảo hành, chính sách vận chuyển, phí ship.
4. KHÁCH HÀNG THÂN THIẾT (KHTT): Điều kiện đăng ký thành viên, các hạng thẻ (Green, Silver, Gold, Diamond), quyền lợi tích điểm, thẻ quà tặng. 4. KHÁCH HÀNG THÂN THIẾT (KHTT): Điều kiện đăng ký thành viên, các hạng thẻ (Green, Silver, Gold, Diamond), quyền lợi tích điểm, thẻ quà tặng.
5. HỖ TRỢ & FAQ: Giải đáp thắc mắc thường gặp, chính sách bảo mật, thông tin liên hệ văn phòng, tuyển dụng. 5. HỖ TRỢ & FAQ: Giải đáp thắc mắc thường gặp, chính sách bảo mật, thông tin liên hệ văn phòng, tuyển dụng.
6. TRA CỨU SIZE (BẢNG KÍCH CỠ): Hướng dẫn chọn size chuẩn cho nam, nữ, trẻ em dựa trên chiều cao, cân nặng. 6. TRA CỨU SIZE (BẢNG KÍCH CỠ): Hướng dẫn chọn size chuẩn cho nam, nữ, trẻ em dựa trên chiều cao, cân nặng.
7. GIẢI NGHĨA TỪ VIẾT TẮT: Tự động hiểu các từ viết tắt phổ biến của khách hàng (ví dụ: 'ct' = 'chương trình khuyến mãi/ưu đãi', 'khtt' = 'khách hàng thân thiết', 'store' = 'cửa hàng', 'đc' = 'địa chỉ'). 7. GIẢI NGHĨA TỪ VIẾT TẮT: Tự động hiểu các từ viết tắt phổ biến của khách hàng (ví dụ: 'ct' = 'chương trình khuyến mãi/ưu đãi', 'khtt' = 'khách hàng thân thiết', 'store' = 'cửa hàng', 'đc' = 'địa chỉ').
Ví dụ các câu hỏi phù hợp: Ví dụ các câu hỏi phù hợp:
- 'Bên bạn đang có ct gì không?' (Hiểu là: Chương trình khuyến mãi) - 'Bên bạn đang có ct gì không?' (Hiểu là: Chương trình khuyến mãi)
- 'Canifa ở Cầu Giấy địa chỉ ở đâu?' - 'Canifa ở Cầu Giấy địa chỉ ở đâu?'
- 'Chính sách đổi trả hàng trong bao nhiêu ngày?' - 'Chính sách đổi trả hàng trong bao nhiêu ngày?'
- 'Làm sao để lên hạng thẻ Gold?' - 'Làm sao để lên hạng thẻ Gold?'
- 'Cho mình xem bảng size áo nam.' - 'Cho mình xem bảng size áo nam.'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?' - 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Canifa thành lập năm nào?' - 'Canifa thành lập năm nào?'
""" """
logger.info(f"🔍 [Semantic Search] Brand Knowledge query: {query}") logger.info(f"🔍 [Semantic Search] Brand Knowledge query: {query}")
try: try:
# 1. Tạo embedding cho câu hỏi (Mặc định 1536 chiều như bro yêu cầu) # 1. Tạo embedding cho câu hỏi (Mặc định 1536 chiều như bro yêu cầu)
query_vector = await create_embedding_async(query) query_vector = await create_embedding_async(query)
if not query_vector: if not query_vector:
return "Xin lỗi, tôi gặp sự cố khi xử lý thông tin. Vui lòng thử lại sau." return "Xin lỗi, tôi gặp sự cố khi xử lý thông tin. Vui lòng thử lại sau."
v_str = "[" + ",".join(str(v) for v in query_vector) + "]" v_str = "[" + ",".join(str(v) for v in query_vector) + "]"
# 2. Query StarRocks lấy Top 4 kết quả phù hợp nhất (Không check score) # 2. Query StarRocks lấy Top 4 kết quả phù hợp nhất (Không check score)
sql = f""" sql = f"""
SELECT SELECT
content, content,
metadata metadata
FROM shared_source.chatbot_rsa_knowledge FROM shared_source.chatbot_rsa_knowledge
ORDER BY approx_cosine_similarity(embedding, {v_str}) DESC ORDER BY approx_cosine_similarity(embedding, {v_str}) DESC
LIMIT 4 LIMIT 4
""" """
sr = get_db_connection() sr = get_db_connection()
results = await sr.execute_query_async(sql) results = await sr.execute_query_async(sql)
if not results: if not results:
logger.warning(f"⚠️ No knowledge data found in DB for query: {query}") logger.warning(f"⚠️ No knowledge data found in DB for query: {query}")
return "Hiện tại tôi chưa tìm thấy thông tin chính xác về nội dung này trong hệ thống kiến thức của Canifa. Bạn có thể liên hệ hotline 1800 6061 để được hỗ trợ trực tiếp." return "Hiện tại tôi chưa tìm thấy thông tin chính xác về nội dung này trong hệ thống kiến thức của Canifa. Bạn có thể liên hệ hotline 1800 6061 để được hỗ trợ trực tiếp."
# 3. Tổng hợp kết quả # 3. Tổng hợp kết quả
knowledge_texts = [] knowledge_texts = []
for i, res in enumerate(results): for i, res in enumerate(results):
content = res.get("content", "") content = res.get("content", "")
knowledge_texts.append(content) knowledge_texts.append(content)
# LOG DỮ LIỆU LẤY ĐƯỢC (Chỉ hiển thị nội dung) # LOG DỮ LIỆU LẤY ĐƯỢC (Chỉ hiển thị nội dung)
logger.info(f"📄 [Knowledge Chunk {i + 1}]: {content[:200]}...") logger.info(f"📄 [Knowledge Chunk {i + 1}]: {content[:200]}...")
final_response = "\n\n---\n\n".join(knowledge_texts) final_response = "\n\n---\n\n".join(knowledge_texts)
logger.info(f"✅ Found {len(results)} relevant knowledge chunks.") logger.info(f"✅ Found {len(results)} relevant knowledge chunks.")
return final_response return final_response
except Exception as e: except Exception as e:
logger.error(f"❌ Error in canifa_knowledge_search: {e}") logger.error(f"❌ Error in canifa_knowledge_search: {e}")
return "Tôi đang gặp khó khăn khi truy cập kho kiến thức. Bạn muốn hỏi về sản phẩm gì khác không?" return "Tôi đang gặp khó khăn khi truy cập kho kiến thức. Bạn muốn hỏi về sản phẩm gì khác không?"
""" """
Tool thu thập thông tin khách hàng (Tên, Số điện thoại, Email) Tool thu thập thông tin khách hàng (Tên, Số điện thoại, Email)
Dùng để đẩy data về CRM hoặc hệ thống lưu trữ khách hàng. Dùng để đẩy data về CRM hoặc hệ thống lưu trữ khách hàng.
""" """
import json import json
import logging import logging
from langchain_core.tools import tool from langchain_core.tools import tool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@tool @tool
async def collect_customer_info(name: str, phone: str, email: str | None) -> str: async def collect_customer_info(name: str, phone: str, email: str | None) -> str:
""" """
Sử dụng tool này để ghi lại thông tin khách hàng khi họ muốn tư vấn sâu hơn, Sử dụng tool này để ghi lại thông tin khách hàng khi họ muốn tư vấn sâu hơn,
nhận khuyến mãi hoặc đăng ký mua hàng. nhận khuyến mãi hoặc đăng ký mua hàng.
Args: Args:
name: Tên của khách hàng name: Tên của khách hàng
phone: Số điện thoại của khách hàng phone: Số điện thoại của khách hàng
email: Email của khách hàng (không bắt buộc) email: Email của khách hàng (không bắt buộc)
""" """
try: try:
print(f"\n[TOOL] --- 📝 Thu thập thông tin khách hàng: {name} - {phone} ---") print(f"\n[TOOL] --- 📝 Thu thập thông tin khách hàng: {name} - {phone} ---")
logger.info(f"📝 Collecting customer info: {name}, {phone}, {email}") logger.info(f"📝 Collecting customer info: {name}, {phone}, {email}")
# Giả lập việc đẩy data đi (CRM/Sheet) # Giả lập việc đẩy data đi (CRM/Sheet)
# Trong thực tế, bạn sẽ gọi một API ở đây # Trong thực tế, bạn sẽ gọi một API ở đây
db_record = { db_record = {
"customer_name": name, "customer_name": name,
"phone_number": phone, "phone_number": phone,
"email_address": email, "email_address": email,
"status": "pending_consultation", "status": "pending_consultation",
} }
# Trả về kết quả thành công # Trả về kết quả thành công
return json.dumps( return json.dumps(
{ {
"status": "success", "status": "success",
"message": ( "message": (
f"Cảm ơn anh/chị {name}. CiCi đã ghi nhận thông tin và sẽ có nhân viên " f"Cảm ơn anh/chị {name}. CiCi đã ghi nhận thông tin và sẽ có nhân viên "
f"liên hệ tư vấn qua số điện thoại {phone} sớm nhất ạ!" f"liên hệ tư vấn qua số điện thoại {phone} sớm nhất ạ!"
), ),
"data_captured": db_record, "data_captured": db_record,
}, },
ensure_ascii=False, ensure_ascii=False,
) )
except Exception as e: except Exception as e:
logger.error(f"❌ Lỗi khi thu thập thông tin: {e}") logger.error(f"❌ Lỗi khi thu thập thông tin: {e}")
return json.dumps( return json.dumps(
{ {
"status": "error", "status": "error",
"message": f"Xin lỗi, CiCi gặp sự cố khi lưu thông tin. Anh/chị vui lòng thử lại sau ạ. Lỗi: {e!s}", "message": f"Xin lỗi, CiCi gặp sự cố khi lưu thông tin. Anh/chị vui lòng thử lại sau ạ. Lỗi: {e!s}",
}, },
ensure_ascii=False, ensure_ascii=False,
) )
This diff is collapsed.
""" """
Tools Factory Tools Factory
Chỉ return 1 tool duy nhất: data_retrieval_tool Chỉ return 1 tool duy nhất: data_retrieval_tool
""" """
from langchain_core.tools import Tool from langchain_core.tools import Tool
from .brand_knowledge_tool import canifa_knowledge_search from .brand_knowledge_tool import canifa_knowledge_search
from .customer_info_tool import collect_customer_info from .customer_info_tool import collect_customer_info
from .data_retrieval_tool import data_retrieval_tool from .data_retrieval_tool import data_retrieval_tool
def get_retrieval_tools() -> list[Tool]: def get_retrieval_tools() -> list[Tool]:
"""Các tool chỉ dùng để đọc/truy vấn dữ liệu (Có thể cache)""" """Các tool chỉ dùng để đọc/truy vấn dữ liệu (Có thể cache)"""
return [data_retrieval_tool, canifa_knowledge_search] return [data_retrieval_tool, canifa_knowledge_search]
def get_collection_tools() -> list[Tool]: def get_collection_tools() -> list[Tool]:
"""Các tool dùng để ghi/thu thập dữ liệu (KHÔNG cache)""" """Các tool dùng để ghi/thu thập dữ liệu (KHÔNG cache)"""
return [collect_customer_info] return [collect_customer_info]
def get_all_tools() -> list[Tool]: def get_all_tools() -> list[Tool]:
"""Return toàn bộ list tools cho Agent""" """Return toàn bộ list tools cho Agent"""
return get_retrieval_tools() + get_collection_tools() return get_retrieval_tools() + get_collection_tools()
This diff is collapsed.
""" """
Cache Analytics API Routes Cache Analytics API Routes
=========================== ===========================
Provides endpoints to monitor semantic cache performance: Provides endpoints to monitor semantic cache performance:
- Cache statistics (hit rate, cost savings, performance) - Cache statistics (hit rate, cost savings, performance)
- Clear user cache - Clear user cache
- Reset statistics - Reset statistics
""" """
import logging import logging
from fastapi import APIRouter from fastapi import APIRouter
from common.cache import clear_user_cache, get_cache_stats, reset_cache_stats from common.cache import clear_user_cache, get_cache_stats, reset_cache_stats
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix="/cache", tags=["Cache Analytics"]) router = APIRouter(prefix="/cache", tags=["Cache Analytics"])
@router.get("/stats") @router.get("/stats")
async def get_cache_statistics(): async def get_cache_statistics():
""" """
Get semantic cache performance statistics. Get semantic cache performance statistics.
Returns: Returns:
Cache stats including: Cache stats including:
- LLM cache hit/miss rates - LLM cache hit/miss rates
- Embedding cache hit/miss rates - Embedding cache hit/miss rates
- Cost savings (USD) - Cost savings (USD)
- Performance metrics (time saved) - Performance metrics (time saved)
Example Response: Example Response:
```json ```json
{ {
"total_queries": 150, "total_queries": 150,
"llm_cache": { "llm_cache": {
"hits": 90, "hits": 90,
"misses": 60, "misses": 60,
"hit_rate_percent": 60.0, "hit_rate_percent": 60.0,
"cost_saved_usd": 0.09 "cost_saved_usd": 0.09
}, },
"embedding_cache": { "embedding_cache": {
"hits": 120, "hits": 120,
"misses": 30, "misses": 30,
"hit_rate_percent": 80.0, "hit_rate_percent": 80.0,
"cost_saved_usd": 0.012 "cost_saved_usd": 0.012
}, },
"performance": { "performance": {
"avg_saved_time_ms": 1850, "avg_saved_time_ms": 1850,
"total_time_saved_seconds": 166.5 "total_time_saved_seconds": 166.5
}, },
"total_cost_saved_usd": 0.102 "total_cost_saved_usd": 0.102
} }
``` ```
""" """
try: try:
stats = await get_cache_stats() stats = await get_cache_stats()
return { return {
"status": "success", "status": "success",
"data": stats, "data": stats,
} }
except Exception as e: except Exception as e:
logger.error(f"Error getting cache stats: {e}", exc_info=True) logger.error(f"Error getting cache stats: {e}", exc_info=True)
return { return {
"status": "error", "status": "error",
"message": str(e), "message": str(e),
} }
@router.delete("/user/{user_id}") @router.delete("/user/{user_id}")
async def clear_cache_for_user(user_id: str): async def clear_cache_for_user(user_id: str):
""" """
Clear all cached responses for a specific user. Clear all cached responses for a specific user.
Args: Args:
user_id: User ID to clear cache for user_id: User ID to clear cache for
Returns: Returns:
Number of cache entries deleted Number of cache entries deleted
Use cases: Use cases:
- User requests to clear their data - User requests to clear their data
- User reports incorrect cached responses - User reports incorrect cached responses
- Manual cache invalidation for testing - Manual cache invalidation for testing
""" """
try: try:
deleted_count = await clear_user_cache(user_id) deleted_count = await clear_user_cache(user_id)
return { return {
"status": "success", "status": "success",
"message": f"Cleared {deleted_count} cache entries for user {user_id}", "message": f"Cleared {deleted_count} cache entries for user {user_id}",
"deleted_count": deleted_count, "deleted_count": deleted_count,
} }
except Exception as e: except Exception as e:
logger.error(f"Error clearing user cache: {e}", exc_info=True) logger.error(f"Error clearing user cache: {e}", exc_info=True)
return { return {
"status": "error", "status": "error",
"message": str(e), "message": str(e),
} }
@router.post("/stats/reset") @router.post("/stats/reset")
async def reset_statistics(): async def reset_statistics():
""" """
Reset cache statistics counters. Reset cache statistics counters.
This resets: This resets:
- Hit/miss counters - Hit/miss counters
- Cost savings calculations - Cost savings calculations
- Performance metrics - Performance metrics
Note: This does NOT delete cached data, only resets the statistics. Note: This does NOT delete cached data, only resets the statistics.
""" """
try: try:
reset_cache_stats() reset_cache_stats()
return { return {
"status": "success", "status": "success",
"message": "Cache statistics reset successfully", "message": "Cache statistics reset successfully",
} }
except Exception as e: except Exception as e:
logger.error(f"Error resetting cache stats: {e}", exc_info=True) logger.error(f"Error resetting cache stats: {e}", exc_info=True)
return { return {
"status": "error", "status": "error",
"message": str(e), "message": str(e),
} }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
services: services:
# --- Backend Service --- # --- Backend Service ---
backend: backend:
build: . build: .
container_name: canifa_backend container_name: canifa_backend
env_file: .env env_file: .env
ports: ports:
- "5000:5000" - "5000:5000"
volumes: volumes:
- .:/app - .:/app
environment: environment:
- PORT=5000 - PORT=5000
restart: unless-stopped restart: unless-stopped
deploy: deploy:
resources: resources:
limits: limits:
memory: 8g memory: 8g
networks: networks:
- backend_network - backend_network
logging: logging:
driver: "json-file" driver: "json-file"
options: options:
tag: "{{.Name}}" tag: "{{.Name}}"
networks: networks:
backend_network: backend_network:
driver: bridge driver: bridge
ipam: ipam:
driver: default driver: default
config: config:
- subnet: "172.24.0.0/16" - subnet: "172.24.0.0/16"
gateway: "172.24.0.1" gateway: "172.24.0.1"
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
#!/bin/bash #!/bin/bash
NUM_CORES=$(nproc) NUM_CORES=$(nproc)
WORKERS=$((2 * NUM_CORES + 1)) WORKERS=$((2 * NUM_CORES + 1))
echo "🔧 [STARTUP] CPU cores: $NUM_CORES" echo "🔧 [STARTUP] CPU cores: $NUM_CORES"
echo "🔧 [STARTUP] Gunicorn workers: $WORKERS" echo "🔧 [STARTUP] Gunicorn workers: $WORKERS"
exec gunicorn \ exec gunicorn \
server:app \ server:app \
--workers "$WORKERS" \ --workers "$WORKERS" \
--worker-class uvicorn.workers.UvicornWorker \ --worker-class uvicorn.workers.UvicornWorker \
--worker-connections 1000 \ --worker-connections 1000 \
--max-requests 1000 \ --max-requests 1000 \
--max-requests-jitter 100 \ --max-requests-jitter 100 \
--timeout 30 \ --timeout 30 \
--access-logfile - \ --access-logfile - \
--error-logfile - \ --error-logfile - \
--bind 0.0.0.0:5000 \ --bind 0.0.0.0:5000 \
--log-level info --log-level info
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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