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

Update project state

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