File size: 7,790 Bytes
8eaa451 f3d50d7 8eaa451 f3d50d7 8eaa451 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """
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)
|