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

Add Docker stage/prod, entrypoint auto-scaling, mock APIs, comment log functions

parent f057ad1e
FROM grafana/k6:latest
# Copy K6 test script
COPY backend/hehe/k6-chatbot-test.js /scripts/chatbot-test.js
# Default command - chạy K6 test
CMD ["run", "--out", "json=/tmp/results.json", "/scripts/chatbot-test.js"]
# ============================================================
# DOCKERFILE.PROD - Production (Multi-Worker Gunicorn)
# ============================================================
# Multi-stage build để optimize dung lượng image
FROM python:3.11-slim as builder
WORKDIR /app
# Copy requirements.txt
COPY requirements.txt .
# Install dependencies to a local directory
RUN pip install --user --no-cache-dir -r requirements.txt
# ============================================================
# Final stage - Production image (tối giản)
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV ENV=production
# Copy Python packages từ builder stage
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# Copy source code
COPY . .
# Copy entrypoint script
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# Expose port 5000
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
ENTRYPOINT ["/app/entrypoint.sh"]
# ============================================================
# DOCKERFILE.STAGE - Development/Staging (Hot Reload)
# ============================================================
# 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
...@@ -7,6 +10,7 @@ WORKDIR /app ...@@ -7,6 +10,7 @@ WORKDIR /app
# Thiết lập biến môi trường để log in ra ngay lập tức # Thiết lập biến môi trường để log in ra ngay lập tức
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
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 .
...@@ -21,5 +25,6 @@ COPY . . ...@@ -21,5 +25,6 @@ COPY . .
# Expose port 5000 (Port chạy server) # Expose port 5000 (Port chạy server)
EXPOSE 5000 EXPOSE 5000
# Lệnh chạy server dùng uvicorn # Lệnh chạy server dùng uvicorn với hot reload
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "5000"] # ⚡ Hot reload - tự động restart khi code thay đổi (dùng cho dev/stage)
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "5000", "--reload"]
"""
Fashion Q&A Agent Controller
Langfuse will auto-trace via LangChain integration (no code changes needed).
"""
import json
import logging
import uuid
from fastapi import BackgroundTasks
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from common.conversation_manager import ConversationManager, get_conversation_manager
from common.langfuse_client import get_callback_handler, langfuse_trace_context
from common.llm_factory import create_llm
from config import DEFAULT_MODEL
from .graph import build_graph
from .models import AgentState, get_config
from .tools.get_tools import get_all_tools
logger = logging.getLogger(__name__)
async def chat_controller(
query: str,
user_id: str,
background_tasks: BackgroundTasks,
model_name: str = DEFAULT_MODEL,
images: list[str] | None = None,
) -> dict:
"""
Controller main logic for non-streaming chat requests.
Langfuse will automatically trace all LangChain operations.
"""
logger.info(f"▶️ Starting chat_controller with model: {model_name} for user: {user_id}")
config = get_config()
config.model_name = model_name
# Enable JSON mode to ensure structured output
llm = create_llm(model_name=model_name, streaming=False, json_mode=True)
tools = get_all_tools()
graph = build_graph(config, llm=llm, tools=tools)
# Init ConversationManager (Singleton)
memory = await get_conversation_manager()
# LOAD HISTORY & Prepare State (Optimize: history logic remains solid)
history_dicts = await memory.get_chat_history(user_id, limit=20)
history = []
for h in reversed(history_dicts):
msg_cls = HumanMessage if h["is_human"] else AIMessage
history.append(msg_cls(content=h["message"]))
initial_state, exec_config = _prepare_execution_context(
query=query, user_id=user_id, history=history, images=images
)
try:
# 🔥 Wrap graph execution với langfuse_trace_context để set user_id cho tất cả observations
with langfuse_trace_context(user_id=user_id, session_id=user_id):
# TỐI ƯU: Chạy Graph
result = await graph.ainvoke(initial_state, config=exec_config)
# TỐI ƯU: Extract IDs từ Tool Messages một lần duy nhất
all_product_ids = _extract_product_ids(result.get("messages", []))
# TỐI ƯU: Xử lý AI Response
ai_raw_content = result.get("ai_response").content if result.get("ai_response") else ""
logger.info(f"💾 [RAW AI OUTPUT]:\n{ai_raw_content}")
# Parse JSON để lấy text response và product_ids từ AI
ai_text_response = ai_raw_content
try:
# Vì json_mode=True, OpenAI sẽ nhả raw JSON
ai_json = json.loads(ai_raw_content)
# Extract text response từ JSON
ai_text_response = ai_json.get("ai_response", ai_raw_content)
# Merge product_ids từ AI JSON (nếu có) - KHÔNG dùng set() vì dict unhashable
explicit_ids = ai_json.get("product_ids", [])
if explicit_ids and isinstance(explicit_ids, list):
# Merge và deduplicate by SKU
seen_skus = {p["sku"] for p in all_product_ids if "sku" in p}
for product in explicit_ids:
if isinstance(product, dict) and product.get("sku") not in seen_skus:
all_product_ids.append(product)
seen_skus.add(product.get("sku"))
except (json.JSONDecodeError, Exception) as e:
# Nếu AI trả về text thường (hiếm khi xảy ra trong JSON mode) thì ignore
logger.warning(f"Could not parse AI response as JSON: {e}")
pass
# BACKGROUND TASK: Lưu history nhanh gọn
background_tasks.add_task(
_handle_post_chat_async,
memory=memory,
user_id=user_id,
human_query=query,
ai_msg=AIMessage(content=ai_text_response),
)
return {
"ai_response": ai_text_response, # CHỈ text, không phải JSON
"product_ids": all_product_ids, # Array of product objects
}
except Exception as e:
logger.error(f"💥 Chat error for user {user_id}: {e}", exc_info=True)
raise
def _extract_product_ids(messages: list) -> list[dict]:
"""
Extract full product info from tool messages (data_retrieval_tool results).
Returns list of product objects with: sku, name, price, sale_price, url, thumbnail_image_url.
"""
products = []
seen_skus = set()
for msg in messages:
if isinstance(msg, ToolMessage):
try:
# Tool result is JSON string
tool_result = json.loads(msg.content)
# Check if tool returned products
if tool_result.get("status") == "success" and "products" in tool_result:
for product in tool_result["products"]:
sku = product.get("internal_ref_code")
if sku and sku not in seen_skus:
seen_skus.add(sku)
# Extract full product info
product_obj = {
"sku": sku,
"name": product.get("magento_product_name", ""),
"price": product.get("price_vnd", 0),
"sale_price": product.get("sale_price_vnd"), # null nếu không sale
"url": product.get("magento_url_key", ""),
"thumbnail_image_url": product.get("thumbnail_image_url", ""),
}
products.append(product_obj)
except (json.JSONDecodeError, KeyError, TypeError) as e:
logger.debug(f"Could not parse tool message for products: {e}")
continue
return products
def _prepare_execution_context(query: str, user_id: str, history: list, images: list | None):
"""Prepare initial state and execution config for the graph run."""
initial_state: AgentState = {
"user_query": HumanMessage(content=query),
"messages": [HumanMessage(content=query)],
"history": history,
"user_id": user_id,
"images_embedding": [],
"ai_response": None,
}
run_id = str(uuid.uuid4())
# Metadata for LangChain (tags for logging/filtering)
metadata = {
"run_id": run_id,
"tags": "chatbot,production",
}
# 🔥 CallbackHandler - sẽ được wrap trong langfuse_trace_context để set user_id
# Per Langfuse docs: propagate_attributes() handles user_id propagation
langfuse_handler = get_callback_handler()
exec_config = RunnableConfig(
configurable={
"user_id": user_id,
"transient_images": images or [],
"run_id": run_id,
},
run_id=run_id,
metadata=metadata,
callbacks=[langfuse_handler] if langfuse_handler else [],
)
return initial_state, exec_config
async def _handle_post_chat_async(
memory: ConversationManager, user_id: str, human_query: str, ai_msg: AIMessage | None
):
"""Save chat history in background task after response is sent."""
if ai_msg:
try:
await memory.save_conversation_turn(user_id, human_query, ai_msg.content)
logger.debug(f"Saved conversation for user {user_id}")
except Exception as e:
logger.error(f"Failed to save conversation for user {user_id}: {e}", exc_info=True)
"""
CANIFA Data Retrieval Tool - Tối giản cho Agentic Workflow.
Hỗ trợ Hybrid Search: Semantic (Vector) + Metadata Filter.
"""
import asyncio
import json
import logging
import time
from decimal import Decimal
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from agent.tools.product_search_helpers import build_starrocks_query
from common.starrocks_connection import StarRocksConnection
# from langsmith import traceable
logger = logging.getLogger(__name__)
class DecimalEncoder(json.JSONEncoder):
"""Xử lý kiểu Decimal từ Database khi convert sang JSON."""
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
class SearchItem(BaseModel):
"""Cấu trúc một mục tìm kiếm đơn lẻ trong Multi-Search."""
query: str = Field(
...,
description="Câu hỏi/mục đích tự do của user (đi chơi, dự tiệc, phỏng vấn,...) - dùng cho Semantic Search",
)
keywords: str | None = Field(
..., description="Từ khóa sản phẩm cụ thể (áo polo, quần jean,...) - dùng cho LIKE search"
)
magento_ref_code: str | None = Field(
..., description="Mã sản phẩm hoặc mã màu/SKU (Ví dụ: 8TS24W001 hoặc 8TS24W001-SK010)."
)
product_line_vn: str | None = Field(..., description="Dòng sản phẩm (Áo phông, Quần short,...)")
gender_by_product: str | None = Field(..., description="Giới tính: male, female")
age_by_product: str | None = Field(..., description="Độ tuổi: adult, kids, baby, others")
master_color: str | None = Field(..., description="Màu sắc chính (Đen/ Black, Trắng/ White,...)")
material_group: str | None = Field(
...,
description="Nhóm chất liệu. BẮT BUỘC dùng đúng: 'Yarn - Sợi', 'Knit - Dệt Kim', 'Woven - Dệt Thoi', 'Knit/Woven - Dệt Kim/Dệt Thoi'.",
)
season: str | None = Field(..., description="Mùa (Spring Summer, Autumn Winter)")
style: str | None = Field(..., description="Phong cách (Basic Update, Fashion,...)")
fitting: str | None = Field(..., description="Form dáng (Regular, Slim, Loose,...)")
form_neckline: str | None = Field(..., description="Kiểu cổ (Crew Neck, V-neck,...)")
form_sleeve: str | None = Field(..., description="Kiểu tay (Short Sleeve, Long Sleeve,...)")
price_min: float | None = Field(..., description="Giá thấp nhất")
price_max: float | None = Field(..., description="Giá cao nhất")
action: str = Field(..., description="Hành động: 'search' (tìm kiếm) hoặc 'visual_search' (phân tích ảnh)")
class MultiSearchParams(BaseModel):
"""Tham số cho Parallel Multi-Search."""
searches: list[SearchItem] = Field(..., description="Danh sách các truy vấn tìm kiếm chạy song song")
@tool(args_schema=MultiSearchParams)
# @traceable(run_type="tool", name="data_retrieval_tool")
async def data_retrieval_tool(searches: list[SearchItem]) -> str:
"""
Siêu công cụ tìm kiếm sản phẩm CANIFA - Hỗ trợ Parallel Multi-Search (Chạy song song nhiều query).
💡 ĐIỂM ĐẶC BIỆT:
Công cụ này cho phép thực hiện NHIỀU truy vấn tìm kiếm CÙNG LÚC.
Hãy dùng nó khi cần SO SÁNH sản phẩm hoặc tìm trọn bộ OUTFIT (mix & match).
⚠️ QUAN TRỌNG - KHI NÀO DÙNG GÌ:
1️⃣ DÙNG 'query' (Semantic Search - BUỘC PHẢI CÓ):
- Áp dụng cho mọi lượt search để cung cấp bối cảnh (context).
- Ví dụ: "áo thun nam đi biển", "quần tây công sở", "đồ cho bé màu xanh"...
2️⃣ DÙNG METADATA FILTERS (Exact/Partial Match):
- Khi khách nói rõ THUỘC TÍNH: Màu sắc, giá, giới tính, độ tuổi, mã sản phẩm.
- **QUY TẮC MÃ SẢN PHẨM:** Mọi loại mã (VD: `8TS...` hoặc `8TS...-SK...`) → Điền vào `magento_ref_code`.
- **QUY TẮC CHẤT LIÊU (material_group):** Chỉ dùng: `Yarn - Sợi`, `Knit - Dệt Kim`, `Woven - Dệt Thoi`, `Knit/Woven - Dệt Kim/Dệt Thoi`.
📝 VÍ DỤ CHI TIẾT (Single Search):
- Example 1: searches=[{"query": "áo polo nam giá dưới 400k", "keywords": "áo polo", "gender_by_product": "male", "price_max": 400000}]
- Example 2: searches=[{"query": "sản phẩm mã 8TS24W001", "magento_ref_code": "8TS24W001"}]
🚀 VÍ DỤ CẤP CAO (Multi-Search Parallel):
- Example 3 - So sánh: "So sánh áo thun nam đen và áo sơ mi trắng dưới 500k"
Tool Call: searches=[
{"query": "áo thun nam màu đen dưới 500k", "keywords": "áo thun", "master_color": "Đen", "gender_by_product": "male", "price_max": 500000},
{"query": "áo sơ mi nam trắng dưới 500k", "keywords": "áo sơ mi", "master_color": "Trắng", "gender_by_product": "male", "price_max": 500000}
]
- Example 4 - Phối đồ: "Tìm cho mình một cái quần jean và một cái áo khoác để đi chơi"
Tool Call: searches=[
{"query": "quần jean đi chơi năng động", "keywords": "quần jean"},
{"query": "áo khoác đi chơi năng động", "keywords": "áo khoác"}
]
- Example 5 - Cả gia đình: "Tìm áo phông màu xanh cho bố, mẹ và bé trai"
Tool Call: searches=[
{"query": "áo phông nam người lớn màu xanh", "keywords": "áo phông", "master_color": "Xanh", "gender_by_product": "male", "age_by_product": "adult"},
{"query": "áo phông nữ người lớn màu xanh", "keywords": "áo phông", "master_color": "Xanh", "gender_by_product": "female", "age_by_product": "adult"},
{"query": "áo phông bé trai màu xanh", "keywords": "áo phông", "master_color": "Xanh", "gender_by_product": "male", "age_by_product": "others"}
]
"""
logger.info("🔧 [DEBUG] data_retrieval_tool STARTED")
try:
logger.info("🔧 [DEBUG] Creating StarRocksConnection instance")
db = StarRocksConnection()
logger.info("🔧 [DEBUG] StarRocksConnection created successfully")
# 0. Log input parameters (Đúng ý bro)
logger.info(f"📥 [Tool Input] data_retrieval_tool received {len(searches)} items:")
for idx, item in enumerate(searches):
logger.info(f" 🔹 Item [{idx}]: {item.dict(exclude_none=True)}")
# 1. Tạo tasks chạy song song (Parallel)
logger.info("🔧 [DEBUG] Creating parallel tasks")
tasks = []
for item in searches:
tasks.append(_execute_single_search(db, item))
logger.info(f"🚀 [Parallel Search] Executing {len(searches)} queries simultaneously...")
logger.info("🔧 [DEBUG] About to call asyncio.gather()")
results = await asyncio.gather(*tasks)
logger.info(f"🔧 [DEBUG] asyncio.gather() completed with {len(results)} results")
# 2. Tổng hợp kết quả
combined_results = []
for i, products in enumerate(results):
combined_results.append(
{
"search_index": i,
"search_criteria": searches[i].dict(exclude_none=True),
"count": len(products),
"products": products,
}
)
return json.dumps({"status": "success", "results": combined_results}, ensure_ascii=False, cls=DecimalEncoder)
except Exception as e:
logger.error(f"Error in Multi-Search data_retrieval_tool: {e}")
return json.dumps({"status": "error", "message": str(e)})
async def _execute_single_search(db: StarRocksConnection, item: SearchItem) -> list[dict]:
"""Thực thi một search query đơn lẻ (Async)."""
try:
logger.info(f"🔧 [DEBUG] _execute_single_search STARTED for query: {item.query[:50] if item.query else 'None'}")
# ⏱️ Timer: Build query (bao gồm embedding nếu có)
query_build_start = time.time()
logger.info("🔧 [DEBUG] Calling build_starrocks_query()")
sql = await build_starrocks_query(item)
query_build_time = (time.time() - query_build_start) * 1000 # Convert to ms
logger.info(f"🔧 [DEBUG] SQL query built, length: {len(sql)}")
logger.info(f"⏱️ [TIMER] Query Build Time (bao gồm embedding): {query_build_time:.2f}ms")
# ⏱️ Timer: Execute DB query
db_start = time.time()
logger.info("🔧 [DEBUG] Calling db.execute_query_async()")
products = await db.execute_query_async(sql)
db_time = (time.time() - db_start) * 1000 # Convert to ms
logger.info(f"🔧 [DEBUG] Query executed, got {len(products)} products")
logger.info(f"⏱️ [TIMER] DB Query Execution Time: {db_time:.2f}ms")
logger.info(f"⏱️ [TIMER] Total Time (Build + DB): {query_build_time + db_time:.2f}ms")
return _format_product_results(products)
except Exception as e:
logger.error(f"Single search error for item {item}: {e}")
return []
def _format_product_results(products: list[dict]) -> list[dict]:
"""Lọc và format kết quả trả về cho Agent."""
allowed_fields = {
"internal_ref_code",
"description_text_full",
}
return [{k: v for k, v in p.items() if k in allowed_fields} for p in products[:5]]
import asyncio
import time import time
from common.embedding_service import create_embedding_async from common.embedding_service import create_embedding_async
...@@ -112,10 +111,10 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -112,10 +111,10 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
""" """
print("✅ [CODE SEARCH] Query built - No vector search needed!") print("✅ [CODE SEARCH] Query built - No vector search needed!")
# Ghi log debug query FULL vào Background Task (Không làm chậm Request) # Ghi log debug query FULL vào Background Task (Không làm chậm Request)
asyncio.create_task(save_query_to_log(sql)) # asyncio.create_task(save_query_to_log(sql))
return sql return sql
# ============================================================ # ============================================================
...@@ -173,60 +172,62 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -173,60 +172,62 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
LIMIT 10 LIMIT 10
""" """
# Ghi log debug query FULL vào Background Task (Không làm chậm Request)
asyncio.create_task(save_query_to_log(sql))
return sql return sql
async def save_query_to_log(sql: str): # ============================================================
"""Lưu query full vào file hyde_pure_query.txt.""" # TEMPORARILY COMMENTED OUT - save_query_to_log
import os # ============================================================
log_path = r"D:\cnf\chatbot_canifa\backend\logs\hyde_pure_query.txt" # async def save_query_to_log(sql: str):
try: # """Lưu query full vào file hyde_pure_query.txt."""
log_dir = os.path.dirname(log_path) # import os
if not os.path.exists(log_dir): # log_path = r"D:\cnf\chatbot_canifa\backend\logs\hyde_pure_query.txt"
os.makedirs(log_dir) # try:
with open(log_path, "w", encoding="utf-8") as f: # log_dir = os.path.dirname(log_path)
f.write(sql) # if not os.path.exists(log_dir):
print(f"💾 Full Query saved to: {log_path}") # os.makedirs(log_dir)
except Exception as e: # with open(log_path, "w", encoding="utf-8") as f:
print(f"Save query log failed: {e}") # f.write(sql)
# print(f"💾 Full Query saved to: {log_path}")
# except Exception as e:
async def save_preview_to_log(search_query: str, products: list[dict]): # print(f"Save query log failed: {e}")
"""Lưu kết quả DB trả về vào db_preview.txt (Format đẹp cho AI)."""
import json
import os # ============================================================
preview_path = r"D:\cnf\chatbot_canifa\backend\logs\db_preview.txt" # TEMPORARILY COMMENTED OUT - save_preview_to_log
try: # ============================================================
log_dir = os.path.dirname(preview_path) # async def save_preview_to_log(search_query: str, products: list[dict]):
if not os.path.exists(log_dir): # """Lưu kết quả DB trả về vào db_preview.txt (Format đẹp cho AI)."""
os.makedirs(log_dir) # import os
# preview_path = r"D:\cnf\chatbot_canifa\backend\logs\db_preview.txt"
with open(preview_path, "a", encoding="utf-8") as f: # try:
f.write(f"\n{'='*60}\n") # log_dir = os.path.dirname(preview_path)
f.write(f"⏰ TIME: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") # if not os.path.exists(log_dir):
f.write(f"🔍 SEARCH: {search_query}\n") # os.makedirs(log_dir)
f.write(f"📊 RESULTS COUNT: {len(products)}\n") #
f.write(f"{'-'*60}\n") # with open(preview_path, "a", encoding="utf-8") as f:
# f.write(f"\n{'='*60}\n")
if not products: # f.write(f"⏰ TIME: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("❌ NO PRODUCTS FOUND\n") # f.write(f"🔍 SEARCH: {search_query}\n")
else: # f.write(f"📊 RESULTS COUNT: {len(products)}\n")
for idx, p in enumerate(products[:5], 1): # f.write(f"{'-'*60}\n")
code = p.get("internal_ref_code", "N/A") #
sale = p.get("sale_price", "N/A") # if not products:
orig = p.get("original_price", "N/A") # f.write("❌ NO PRODUCTS FOUND\n")
disc = p.get("discount_amount", "0") # else:
score = p.get("max_score", p.get("similarity_score", "N/A")) # for idx, p in enumerate(products[:5], 1):
desc = p.get("description_text_full", "No Description") # code = p.get("internal_ref_code", "N/A")
# sale = p.get("sale_price", "N/A")
f.write(f"{idx}. [{code}] Score: {score}\n") # orig = p.get("original_price", "N/A")
f.write(f" 💰 Price: {sale} (Orig: {orig}, Disc: {disc}%)\n") # disc = p.get("discount_amount", "0")
f.write(f" 📝 Desc: {desc}\n") # score = p.get("max_score", p.get("similarity_score", "N/A"))
# desc = p.get("description_text_full", "No Description")
f.write(f"{'='*60}\n") #
print(f"💾 DB Preview (Results) saved to: {preview_path}") # f.write(f"{idx}. [{code}] Score: {score}\n")
except Exception as e: # f.write(f" 💰 Price: {sale} (Orig: {orig}, Disc: {disc}%)\n")
print(f"Save preview log failed: {e}") # f.write(f" 📝 Desc: {desc}\n")
#
# f.write(f"{'='*60}\n")
# print(f"💾 DB Preview (Results) saved to: {preview_path}")
# except Exception as e:
# print(f"Save preview log failed: {e}")
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -6,12 +6,17 @@ services: ...@@ -6,12 +6,17 @@ services:
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:
resources:
limits:
memory: 8g
logging: logging:
driver: "json-file" driver: "json-file"
options: options:
tag: "{{.Name}}" tag: "{{.Name}}"
=
# Lấy số CPU cores
NUM_CORES=$(nproc)
WORKERS=$((2 * NUM_CORES + 1))
echo "🔧 [STARTUP] Detected CPU cores: $NUM_CORES"
echo "🔧 [STARTUP] Gunicorn workers: $WORKERS"
echo "🔧 [STARTUP] Environment: ${ENV:-production}"
# Chạy Gunicorn với số workers tính toán được
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
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
...@@ -50,10 +50,10 @@ app.add_middleware( ...@@ -50,10 +50,10 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.include_router(conservation_router)
app.include_router(conservation_router) app.include_router(conservation_router)
app.include_router(chatbot_router) app.include_router(chatbot_router)
# --- MOCK API FOR LOAD TESTING --- # --- MOCK API FOR LOAD TESTING ---
try: try:
from api.mock_api_route import router as mock_router from api.mock_api_route import router as mock_router
......
version: '3.8'
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n_local
ports:
- "5678:5678"
volumes:
- n8n_data:/home/node/.n8n
environment:
- N8N_HOST=localhost
- N8N_PORT=5678
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://localhost:5678/
- GENERIC_TIMEZONE=Asia/Ho_Chi_Minh
restart: unless-stopped
volumes:
n8n_data:
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