| import asyncio
|
| from typing import Optional
|
| from dotenv import load_dotenv
|
| import psutil
|
| import os
|
|
|
| load_dotenv()
|
|
|
| from fastapi.responses import JSONResponse
|
| import logfire
|
|
|
| from fastapi import FastAPI
|
| from fastapi.concurrency import asynccontextmanager
|
| from fastapi.security import HTTPBearer
|
| from src.database.db import Base, engine
|
| from src.errors import DatabaseConnectionError
|
| from src.core.logger import SingletonLogger
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from src.core.chat_engine.query import ChatEngine
|
| from src.core.chat_engine.agent_state import AgentState
|
| from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
| from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| from src.lib.qdrant import ensure_collections_exist
|
| from src.core.background_tasks import backfill_missing_keywords
|
| from src.core.catalog_engine.update import run_daily_catalog_update
|
| from src.router.auth import router as auth_router
|
| from src.router.profile import router as profile_router
|
| from src.router.arxiv import router as arxiv_router
|
| from src.router.paper import router as paper_router
|
| from src.router.ingestion import router as ingestion_router
|
| from src.router.summary import router as summary_router
|
| from src.router.chat import router as chat_router
|
| from src.router.message import router as message_router
|
| from src.router.session import router as session_router
|
| from src.router.admin import router as admin_router
|
| from src.router.user_settings import router as user_settings_router
|
|
|
| from src.model import *
|
|
|
|
|
| @asynccontextmanager
|
| async def lifespan(app: FastAPI):
|
| """Lifespan handler to initialize app state, create DB tables and shutdown cleanup."""
|
| logger = SingletonLogger().get_logger()
|
|
|
| try:
|
| async with engine.begin() as conn:
|
| await conn.run_sync(Base.metadata.create_all)
|
| except Exception as e:
|
| try:
|
| logger = app.state.logger
|
| except Exception:
|
| logger = SingletonLogger().get_logger()
|
| logger.error(f"Error creating database tables: {e}")
|
| raise
|
|
|
| try:
|
| await asyncio.to_thread(ensure_collections_exist)
|
| logger.info("Qdrant collections verified.")
|
| except Exception as e:
|
| logger.warning(f"Qdrant collection setup failed (non-fatal): {e}")
|
|
|
| try:
|
| backfilled = await backfill_missing_keywords()
|
| if backfilled:
|
| logger.info(f"Backfilled keywords for {backfilled} papers on startup.")
|
| except Exception as e:
|
| logger.warning(f"Keyword backfill failed (non-fatal): {e}")
|
|
|
| scheduler = AsyncIOScheduler(timezone="UTC")
|
| scheduler.add_job(
|
| run_daily_catalog_update,
|
| "cron",
|
| hour=2,
|
| minute=0,
|
| misfire_grace_time=3600,
|
| max_instances=1,
|
| replace_existing=True,
|
| id="daily_catalog_update",
|
| )
|
| scheduler.start()
|
| logger.info("APScheduler started with daily catalog update at 02:00 UTC.")
|
|
|
| try:
|
|
|
| app.state.graph = await ChatEngine.build_graph()
|
| logger.info("Chat engine graph successfully built and initialized.")
|
| yield
|
| logger.info("Shutting down application")
|
|
|
| except Exception as e:
|
| logger.error(f"Error during application startup: {e}")
|
| raise
|
| finally:
|
| scheduler.shutdown(wait=False)
|
| logger.info("APScheduler shut down.")
|
|
|
|
|
| app = FastAPI(
|
| title="The Arxplorer - Backend",
|
| description="FastAPI backend application for The Arxplorer. Supports user authentication, question answering from various Arxiv papers.",
|
| version="0.0.1",
|
| lifespan=lifespan,
|
| )
|
| security_scheme = HTTPBearer()
|
|
|
| ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:8080,http://localhost:5173").split(",")
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=ALLOWED_ORIGINS,
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
| app.openapi_components = {
|
| "securitySchemes": {
|
| "BearerAuth": {
|
| "type": "http",
|
| "scheme": "bearer",
|
| "bearerFormat": "JWT",
|
| }
|
| }
|
| }
|
| app.openapi_security = [{"BearerAuth": []}]
|
| logfire.instrument_fastapi(app, capture_headers=True)
|
| logfire.instrument_sqlalchemy(engine)
|
| logfire.instrument_httpx()
|
| logfire.instrument_requests()
|
| logfire.instrument_system_metrics(base="full")
|
|
|
|
|
| app.include_router(auth_router, prefix="/api/v1/auth", tags=["Authentication"])
|
| app.include_router(profile_router, prefix="/api/v1/profile", tags=["Profile"])
|
| app.include_router(arxiv_router, prefix="/api/v1/arxiv", tags=["Arxiv"])
|
| app.include_router(paper_router, prefix="/api/v1/papers", tags=["Papers"])
|
| app.include_router(ingestion_router, prefix="/api/v1/ingestion", tags=["Ingestion"])
|
| app.include_router(summary_router, prefix="/api/v1/summary", tags=["Summary"])
|
| app.include_router(chat_router, prefix="/api/v1/chat", tags=["Chat"])
|
| app.include_router(message_router, prefix="/api/v1/messages", tags=["Messages"])
|
| app.include_router(session_router, prefix="/api/v1/sessions", tags=["Sessions"])
|
| app.include_router(
|
| user_settings_router, prefix="/api/v1/settings", tags=["User Settings"]
|
| )
|
| app.include_router(admin_router, prefix="/admin", tags=["Admin"])
|
|
|
|
|
| @app.get("/health")
|
| def health_check():
|
| cpu_usage: Optional[float] = psutil.cpu_percent(interval=None)
|
| memory_usage: Optional[float] = psutil.virtual_memory().percent
|
| num_threads: Optional[int] = psutil.cpu_count()
|
|
|
| return {
|
| "health": "ok",
|
| "cpu_usage": cpu_usage,
|
| "memory_usage": memory_usage,
|
| "num_threads": num_threads,
|
| }
|
|
|
|
|
| @app.get("/")
|
| def root():
|
| return {"message": "Welcome to The Arxplorer Backend API"}
|
|
|
|
|
| @app.exception_handler(DatabaseConnectionError)
|
| async def db_connection_exception_handler(request, exc: DatabaseConnectionError):
|
| logger = getattr(request.app.state, "logger", SingletonLogger().get_logger())
|
| logger.error("Database connection error: %s", exc)
|
| return JSONResponse(
|
| status_code=503,
|
| content={
|
| "detail": "Database connection error. Please try again later.",
|
| "error": str(exc),
|
| },
|
| )
|
|
|