arxplorer / main.py
Subhadeep Mandal
Fixed db bugs
4c719e5
Raw
History Blame Contribute Delete
6.34 kB
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:
# Build and compile the graph
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) # Non-blocking
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),
},
)