| """ |
| 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 |
|
|
| |
| |
| |
| try: |
| __import__("pysqlite3") |
| import sys |
| sys.modules["sqlite3"] = sys.modules.pop("pysqlite3") |
| except ImportError: |
| pass |
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| """Application startup and shutdown lifecycle.""" |
| logger.info("Initializing DeepMed-AI System (TTYT Thanh Ba)...") |
|
|
| |
| db_service.init_db() |
| logger.info("Database initialized at %s", CHAT_DB_PATH) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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...") |
|
|
|
|
| |
| 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, |
| ) |
|
|
| |
| _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_secret = os.getenv("SESSION_SECRET", secrets.token_hex(32)) |
| app.add_middleware(SessionMiddleware, secret_key=_session_secret) |
|
|
| |
| app.include_router(api_router) |
|
|
| |
| 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): |
| |
| 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) |
| |
| 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) |
|
|
|
|
| |
| if __name__ == "__main__": |
| import uvicorn |
|
|
| uvicorn.run(app, host="0.0.0.0", port=8000) |
|
|