Decoder2704/python-chatbot-jay
0
1"""2FastAPI entrypoint: loads retrieval artifacts at startup (Chatbot-V5.ipynb parity).3"""4 5from __future__ import annotations6 7import logging8import os9from contextlib import asynccontextmanager10 11from fastapi import FastAPI12from fastapi.middleware.cors import CORSMiddleware13 14from .frontend_spa import register_frontend_spa15from .models.schemas import HealthResponse16from .paths import (17 ARTIFACTS_DIR,18 ARTIFACT_CHATBOT_DF_PATH,19 ARTIFACT_QUESTION_VECTORS_PATH,20 ARTIFACT_VECTORIZER_PATH,21 BACKEND_DIR,22 ensure_data_and_artifacts_dirs,23 resolve_frontend_dist_dir,24)25from .routes import chat26from .services import retriever27 28logging.basicConfig(29 level=logging.INFO,30 format="%(levelname)s: %(name)s: %(message)s",31)32logger = logging.getLogger(__name__)33 34 35def _log_artifact_filesystem_probe() -> None:36 """Deployment diagnostics only — does not change retrieval behavior."""37 root = BACKEND_DIR.resolve()38 art = ARTIFACTS_DIR.resolve()39 logger.info("Deploy probe: BACKEND_DIR=%s", root)40 logger.info("Deploy probe: ARTIFACTS_DIR=%s exists=%s", art, art.is_dir())41 if art.is_dir():42 try:43 names = sorted(p.name for p in art.iterdir() if p.is_file())44 logger.info("Deploy probe: artifact directory file names: %s", names)45 except OSError as e:46 logger.warning("Deploy probe: could not list artifact directory: %s", e)47 for label, path in (48 ("vectorizer.pkl", ARTIFACT_VECTORIZER_PATH),49 ("question_vectors.pkl", ARTIFACT_QUESTION_VECTORS_PATH),50 ("chatbot_df.pkl", ARTIFACT_CHATBOT_DF_PATH),51 ):52 try:53 exists = path.is_file()54 size = path.stat().st_size if exists else None55 logger.info("Deploy probe: %s path=%s exists=%s bytes=%s", label, path, exists, size)56 except OSError as e:57 logger.warning("Deploy probe: %s stat failed: %s", label, e)58 59 60def _cors_allow_origins() -> list[str]:61 """Local dev origins plus optional comma-separated production origins (CORS_EXTRA_ORIGINS)."""62 base = [63 "http://localhost:3000",64 "http://127.0.0.1:3000",65 "http://localhost:5173",66 "http://127.0.0.1:5173",67 ]68 extra = os.environ.get("CORS_EXTRA_ORIGINS", "")69 if not extra.strip():70 return base71 more = [o.strip() for o in extra.split(",") if o.strip()]72 seen: set[str] = set()73 out: list[str] = []74 for o in base + more:75 if o not in seen:76 seen.add(o)77 out.append(o)78 return out79 80 81@asynccontextmanager82async def lifespan(app: FastAPI):83 ensure_data_and_artifacts_dirs()84 _log_artifact_filesystem_probe()85 logger.info(86 "Startup: loading retrieval artifacts from %s (data dir: %s)",87 retriever.ARTIFACTS_DIR,88 retriever.DATA_DIR,89 )90 retriever.load_artifacts()91 logger.info("Startup complete")92 yield93 logger.info("Shutdown")94 95 96app = FastAPI(97 title="PyBot API",98 description="REST API mirroring Chatbot-V5.ipynb retrieval flow",99 lifespan=lifespan,100)101 102app.add_middleware(103 CORSMiddleware,104 allow_origins=_cors_allow_origins(),105 allow_credentials=False,106 allow_methods=["*"],107 allow_headers=["*"],108)109 110app.include_router(chat.router)111 112 113@app.get("/health", response_model=HealthResponse, tags=["health"])114async def health():115 return HealthResponse(status="ok")116 117 118# React SPA after all API routes: /chat, /health, /docs, openapi.json, etc. stay on FastAPI.119_spa_dir = resolve_frontend_dist_dir()120if _spa_dir is not None:121 register_frontend_spa(app, _spa_dir)122else:123 logger.warning(124 "No React build found — API only. Build to frontend/dist or frontend/build, "125 "or set FRONTEND_DIST_DIR (see app.paths.resolve_frontend_dist_dir).",126 )127 