DeepMedAI / backend /app /main.py
PBThuong's picture
feat: move ChromaDB to HF Dataset (PBThuong/deepmed-chroma-db)
f3d50d7
Raw
History Blame Contribute Delete
7.79 kB
"""
DeepMed-AI Backend — main.py
FastAPI application entry point: app setup, lifespan, chat initialization.
Module layout:
core/ — config, logging
services/ — ChatService (HybridRAG), DatabaseService
db/ — SQLAlchemy session factory
models/ — ORM models
schemas/ — Pydantic request/response schemas
api/v1/endpoints/ — health, chat, session route handlers
api/v1/api.py — router aggregator
main.py — FastAPI app + lifespan ← you are here
"""
import asyncio
import os
import secrets
from contextlib import asynccontextmanager
# ── Fix SQLite for ChromaDB (MUST be before any chromadb import) ───────────────
# Python 3.10-slim ships SQLite 3.34, but ChromaDB requires >= 3.35.
# pysqlite3-binary provides a newer SQLite; we monkey-patch it in.
try:
__import__("pysqlite3")
import sys
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
except ImportError:
pass # Not needed on systems with SQLite >= 3.35 (e.g., Windows, newer Linux)
# ── Fix HuggingFace cache path (PHẢI đặt trước mọi import khác) ───────────────
# HF Spaces set HOME=/nonexistent → sentence-transformers/transformers crash.
# /tmp luôn writable trong Docker container trên HF Spaces.
os.environ.setdefault("HF_HOME", "/tmp/huggingface")
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", "/tmp/huggingface/hub")
os.environ.setdefault("SENTENCE_TRANSFORMERS_HOME", "/tmp/sentence-transformers")
os.environ.setdefault("TRANSFORMERS_CACHE", "/tmp/transformers")
os.environ.setdefault("XDG_CACHE_HOME", "/tmp/.cache")
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from app.api.v1.api import api_router
from app.core.config import CHAT_DB_PATH, CHROMA_DB_PATH, CHROMA_HF_DATASET, DATA_DIR
from app.core.logging_config import logger
from app.services.chat_service import chat_service
from app.services.database_service import db_service
# ── Lifespan ───────────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application startup and shutdown lifecycle."""
logger.info("Initializing DeepMed-AI System (TTYT Thanh Ba)...")
# 1. Initialize database
db_service.init_db()
logger.info("Database initialized at %s", CHAT_DB_PATH)
# 2. LFS pointer detection (early warning)
if os.path.exists(DATA_DIR):
lfs_pointers = []
for root, _dirs, files in os.walk(DATA_DIR):
for fname in files:
if fname.endswith((".pdf", ".docx", ".xlsx")):
fp = os.path.join(root, fname)
if os.path.getsize(fp) < 500:
lfs_pointers.append(fp)
if lfs_pointers:
logger.error(
"LFS POINTER STUBS DETECTED — %d data file(s) are Git LFS pointers, "
"not real content. RAG will be EMPTY!\n"
"Fix: run `git lfs pull` in the project root, then rebuild the Docker image.\n"
"Affected files:\n %s",
len(lfs_pointers),
"\n ".join(lfs_pointers),
)
else:
logger.warning("DATA directory not found at %s — RAG will have no data", DATA_DIR)
# 3. Download ChromaDB from HuggingFace Dataset if not present locally
if not os.path.exists(CHROMA_DB_PATH) or not os.listdir(CHROMA_DB_PATH):
logger.info("ChromaDB not found locally, downloading from HF Dataset: %s", CHROMA_HF_DATASET)
try:
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=CHROMA_HF_DATASET,
repo_type="dataset",
local_dir=CHROMA_DB_PATH,
local_dir_use_symlinks=False,
)
logger.info("ChromaDB downloaded successfully to %s", CHROMA_DB_PATH)
except Exception as e:
logger.error("Failed to download ChromaDB from HF Dataset: %s", e)
else:
logger.info("ChromaDB already exists at %s", CHROMA_DB_PATH)
# 4. Initialize HybridRAG bot (ChromaDB + BM25 + Reranker)
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, chat_service.initialize)
logger.info("DeepMed-AI System Ready!")
yield
logger.info("Shutting down DeepMed-AI...")
# ── FastAPI App ────────────────────────────────────────────────────────────────
app = FastAPI(
title="DeepMed-AI API",
description="Trí tuệ nhân tạo của TTYT khu vực Thanh Ba",
version="3.0.0",
lifespan=lifespan,
)
# ── CORS ────────────────────────────────────────────────────────────────────────
_raw_origins = os.getenv(
"ALLOWED_ORIGINS",
"http://localhost:5173,http://localhost:3000,http://localhost:7860",
)
_allowed_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=_allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "X-Requested-With"],
)
# ── Session ────────────────────────────────────────────────────────────────────
_session_secret = os.getenv("SESSION_SECRET", secrets.token_hex(32))
app.add_middleware(SessionMiddleware, secret_key=_session_secret)
# ── API Routes ─────────────────────────────────────────────────────────────────
app.include_router(api_router)
# ── Serve React Frontend (SPA) ─────────────────────────────────────────────────
STATIC_DIR = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "static")
)
if os.path.isdir(STATIC_DIR):
assets_dir = os.path.join(STATIC_DIR, "assets")
if os.path.isdir(assets_dir):
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/{path:path}")
async def serve_static(path: str):
# Never intercept API routes — let 404 bubble up naturally
if path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not found")
file_path = os.path.join(STATIC_DIR, path)
if os.path.isfile(file_path):
return FileResponse(file_path)
# SPA fallback: return index.html for client-side routing
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
else:
logger.warning("Static directory not found at %s. Frontend will not be served.", STATIC_DIR)
# ── Entry Point ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)