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

refactor: optimize server setup (dev/prod mode), fix hybrid search columns, debug query logging

parent 1b9277bb
...@@ -2,3 +2,4 @@ ...@@ -2,3 +2,4 @@
# Ignore embedded repo # Ignore embedded repo
preference/ preference/
query.txt
# Python 3.11 slim (nhẹ, ít file)
FROM python:3.11-slim FROM python:3.11-slim
# Thư mục làm việc
WORKDIR /app WORKDIR /app
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
# Copy requirements rồi cài package
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy code
COPY . . COPY . .
# Mở port (sẽ được override bởi docker-compose)
EXPOSE 5004 EXPOSE 5004
# Chạy uvicorn với auto workers (2 * CPU cores + 1) CMD ["python", "server.py"]
CMD ["sh", "-c", "uvicorn server:app --host 0.0.0.0 --port ${PORT:-5004} --workers $(( $(nproc) * 2 + 1 ))"] \ No newline at end of file
\ No newline at end of file
...@@ -37,6 +37,7 @@ __all__ = [ ...@@ -37,6 +37,7 @@ __all__ = [
"MONGODB_URI", "MONGODB_URI",
"OPENAI_API_KEY", "OPENAI_API_KEY",
"PORT", "PORT",
"ENV_MODE",
"REDIS_HOST", "REDIS_HOST",
"REDIS_PASSWORD", "REDIS_PASSWORD",
"REDIS_PORT", "REDIS_PORT",
...@@ -74,6 +75,7 @@ JWT_ALGORITHM: str | None = os.getenv("JWT_ALGORITHM") ...@@ -74,6 +75,7 @@ JWT_ALGORITHM: str | None = os.getenv("JWT_ALGORITHM")
# ====================== SERVER CONFIG ====================== # ====================== SERVER CONFIG ======================
PORT: int = int(os.getenv("PORT", "5004")) PORT: int = int(os.getenv("PORT", "5004"))
ENV_MODE: str = os.getenv("ENV_MODE", "dev").lower() # dev or prod
FIRECRAWL_API_KEY: str | None = os.getenv("FIRECRAWL_API_KEY") FIRECRAWL_API_KEY: str | None = os.getenv("FIRECRAWL_API_KEY")
# ====================== LANGFUSE CONFIGURATION (DEPRECATED) ====================== # ====================== LANGFUSE CONFIGURATION (DEPRECATED) ======================
......
...@@ -127,7 +127,6 @@ async def hybrid_search( ...@@ -127,7 +127,6 @@ async def hybrid_search(
/*+ SET_VAR(ann_params='{{"ef_search":{ef_search}}}') */ /*+ SET_VAR(ann_params='{{"ef_search":{ef_search}}}') */
internal_ref_code, internal_ref_code,
product_name, product_name,
description_text,
product_image_url, product_image_url,
product_web_url, product_web_url,
sale_price, sale_price,
...@@ -147,18 +146,12 @@ async def hybrid_search( ...@@ -147,18 +146,12 @@ async def hybrid_search(
SELECT SELECT
internal_ref_code, internal_ref_code,
MAX_BY(product_name, similarity_score) as product_name, MAX_BY(product_name, similarity_score) as product_name,
MAX_BY(description_text, similarity_score) as description_text,
MAX_BY(product_image_url, similarity_score) as product_image_url, MAX_BY(product_image_url, similarity_score) as product_image_url,
MAX_BY(product_web_url, similarity_score) as product_web_url, MAX_BY(product_web_url, similarity_score) as product_web_url,
MAX_BY(sale_price, similarity_score) as sale_price, MAX_BY(sale_price, similarity_score) as sale_price,
MAX_BY(original_price, similarity_score) as original_price, MAX_BY(original_price, similarity_score) as original_price,
MAX_BY(discount_amount, similarity_score) as discount_amount, MAX_BY(discount_amount, similarity_score) as discount_amount,
GROUP_CONCAT(DISTINCT master_color ORDER BY master_color SEPARATOR ', ') as available_colors, GROUP_CONCAT(DISTINCT master_color ORDER BY master_color SEPARATOR ', ') as available_colors,
MAX_BY(season, similarity_score) as season,
MAX_BY(gender_by_product, similarity_score) as gender_by_product,
MAX_BY(age_by_product, similarity_score) as age_by_product,
MAX_BY(product_line_vn, similarity_score) as product_line_vn,
MAX_BY(style, similarity_score) as style,
MAX(similarity_score) as similarity_score MAX(similarity_score) as similarity_score
FROM semantic_candidates FROM semantic_candidates
{where_clause} {where_clause}
...@@ -168,6 +161,20 @@ async def hybrid_search( ...@@ -168,6 +161,20 @@ async def hybrid_search(
""" """
logger.info(f"SQL Query:\n{sql}") logger.info(f"SQL Query:\n{sql}")
# Ghi query ra file query.txt ở thư mục backend (parent của thư mục search)
try:
import os
# backend/search/hybrid_search.py -> backend/query.txt
current_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.dirname(current_dir)
query_file_path = os.path.join(backend_dir, "query.txt")
with open(query_file_path, "w", encoding="utf-8") as f:
f.write(sql)
logger.info(f"Query saved to: {query_file_path}")
except Exception as e:
logger.error(f"Failed to write query to file: {e}")
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
# STEP 6: Execute query # STEP 6: Execute query
...@@ -230,7 +237,6 @@ async def semantic_only_search( ...@@ -230,7 +237,6 @@ async def semantic_only_search(
season, season,
gender_by_product, gender_by_product,
age_by_product, age_by_product,
product_line_vn,
approx_cosine_similarity(vector, {v_str}) as similarity_score approx_cosine_similarity(vector, {v_str}) as similarity_score
FROM shared_source.magento_product_dimension_with_text_embedding FROM shared_source.magento_product_dimension_with_text_embedding
ORDER BY similarity_score DESC ORDER BY similarity_score DESC
......
""" """
FastAPI main application - Contract AI Service FastAPI Server - Canifa Product Recommendation API
Architecture:
- REST API Routes: FastAPI routers for HTTP endpoints
- SSE (Server-Sent Events): Real-time streaming for AI Contract and AI Legal
Modules:
- ai_contract: Contract generation and composition (SSE streaming)
- ai_legal: Legal Q&A and search (SSE streaming)
- conversation: Conversation history management
""" """
import os import os
import logging import logging
# Configure Logging from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from config import (
PORT,
ENV_MODE,
LANGSMITH_API_KEY,
LANGSMITH_ENDPOINT,
LANGSMITH_PROJECT,
LANGSMITH_TRACING,
)
from api.recommend_text import router as recommend_text_router
# Logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
...@@ -22,35 +28,20 @@ logging.basicConfig( ...@@ -22,35 +28,20 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from config import LANGSMITH_API_KEY, LANGSMITH_ENDPOINT, LANGSMITH_PROJECT, LANGSMITH_TRACING # LangSmith environment
# Ensure LangSmith Env Vars are set for the process
os.environ["LANGSMITH_TRACING"] = LANGSMITH_TRACING os.environ["LANGSMITH_TRACING"] = LANGSMITH_TRACING
os.environ["LANGSMITH_ENDPOINT"] = LANGSMITH_ENDPOINT os.environ["LANGSMITH_ENDPOINT"] = LANGSMITH_ENDPOINT
os.environ["LANGSMITH_API_KEY"] = LANGSMITH_API_KEY os.environ["LANGSMITH_API_KEY"] = LANGSMITH_API_KEY
os.environ["LANGSMITH_PROJECT"] = LANGSMITH_PROJECT os.environ["LANGSMITH_PROJECT"] = LANGSMITH_PROJECT
# FastAPI app
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles # Import StaticFiles
# from api.recommend_image import router as recommend_image_router
from api.recommend_text import router as recommend_text_router
from common.middleware import ClerkAuthMiddleware
from config import PORT
app = FastAPI( app = FastAPI(
title="Contract AI Service", title="Canifa Product Recommendation API",
description="API for Contract AI Service", description="API for product recommendation using hybrid search",
version="1.0.0", version="1.0.0",
) )
print("✅ Clerk Authentication middleware DISABLED (for testing)") # CORS
# Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
...@@ -59,51 +50,42 @@ app.add_middleware( ...@@ -59,51 +50,42 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# app.include_router(recommend_image_router) # Routes
app.include_router(recommend_text_router) app.include_router(recommend_text_router)
try: # Static files
static_dir = os.path.join(os.path.dirname(__file__), "static") static_dir = os.path.join(os.path.dirname(__file__), "static")
if not os.path.exists(static_dir): if not os.path.exists(static_dir):
os.makedirs(static_dir) os.makedirs(static_dir)
app.mount("/static", StaticFiles(directory=static_dir, html=True), name="static") app.mount("/static", StaticFiles(directory=static_dir, html=True), name="static")
print(f"✅ Static files mounted at /static (Dir: {static_dir})")
except Exception as e:
print(f"⚠️ Failed to mount static files: {e}")
from fastapi.responses import RedirectResponse
@app.get("/") @app.get("/")
async def root(): async def root():
return RedirectResponse(url="/static/index.html") return RedirectResponse(url="/static/index.html")
if __name__ == "__main__":
print("=" * 60)
print("🚀 Contract AI Service Starting...")
print("=" * 60)
print(f"📡 REST API: http://localhost:{PORT}")
print(f"📡 SSE Streaming: http://localhost:{PORT}/api/ai-contract/chat")
print(f"📡 Test Chatbot: http://localhost:{PORT}/static/index.html")
print(f"📚 API Docs: http://localhost:{PORT}/docs")
print("=" * 60)
# ENABLE_RELOAD = os.getenv("ENABLE_RELOAD", "false").lower() in ("true", "1", "yes") if __name__ == "__main__":
ENABLE_RELOAD = True import uvicorn
print("⚠️ Hot reload: FORCED ON (Dev Mode)")
reload_dirs = [ is_dev = ENV_MODE != "prod"
"ai_contract",
"conversation", logger.info(f"Starting server on port {PORT} (ENV_MODE={ENV_MODE})")
"common",
"api", # Watch api folder if is_dev:
"agent" # Watch agent folder uvicorn.run(
] "server:app",
host="0.0.0.0",
uvicorn.run( port=PORT,
"server:app", reload=True,
host="0.0.0.0", reload_dirs=["."],
port=PORT, log_level="info",
reload=ENABLE_RELOAD, )
reload_dirs=reload_dirs, else:
log_level="info", uvicorn.run(
) "server:app",
host="0.0.0.0",
port=PORT,
workers=os.cpu_count() * 2 + 1,
log_level="info",
)
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