Spaces:
Running
Running
File size: 4,201 Bytes
a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 5688c6d a32aee9 5688c6d f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c 5688c6d f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 | 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 | """
ResearchRAG FastAPI application.
A thin HTTP wrapper around the existing ``app/`` backend so a React frontend
can use every feature the Streamlit app has. Run with:
uvicorn api.main:app --host 0.0.0.0 --port 8000
The Streamlit app (``streamlit_app.py``) is unaffected and can keep running
independently against the same data.
Security posture:
- every data route requires a bearer token (``api.security.current_user``)
- the user id is derived from that token only, never from the request body
- CORS is an explicit allowlist and fails closed outside development
- all endpoints are rate limited; errors use one envelope and leak nothing
"""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi.middleware import SlowAPIMiddleware
from app.auth import init_auth_db
from app.config import get_settings
from app.database import init_chroma
from app.reranker import warm_reranker
from app.sessions import init_sessions_db, purge_expired
from api.errors import register_exception_handlers
from api.middleware import RequestContextMiddleware, configure_logging
from api.rate_limit import limiter
from api.routers import auth, chat, documents, meta, openalex, search
from api.settings import get_api_settings
logger = logging.getLogger("researchrag.api")
api_settings = get_api_settings()
# Fail fast on missing production configuration (JWT secret, CORS allowlist)
# rather than booting with insecure defaults.
api_settings.validate_runtime()
configure_logging(json_output=api_settings.is_production)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Same startup work the Streamlit app performs in its cached startup(),
# plus the refresh-token table used only by the API.
init_chroma()
init_auth_db()
init_sessions_db()
# Warm the cross-encoder here too. init_chroma()/get_embedder() covered the
# embedder but not the reranker, so its ~500 MB load landed on whichever
# request arrived first β and on a cold container several concurrent first
# requests each started their own copy.
_cfg = get_settings()
if getattr(_cfg, "enable_reranker", False):
warm_reranker(_cfg.reranker_model)
removed = purge_expired()
logger.info(
"api_ready",
extra={"request_id": "-", "path": f"env={api_settings.environment} purged={removed}"},
)
yield
app = FastAPI(
title=api_settings.api_title,
version=api_settings.api_version,
description=(
"HTTP API wrapping the ResearchRAG backend (OpenAlex search, PDF "
"ingestion, hybrid RAG with streaming, semantic search, auth). "
"All data endpoints require a bearer token."
),
lifespan=lifespan,
docs_url=api_settings.docs_url,
redoc_url=None,
openapi_url=api_settings.openapi_url,
)
# ββ Middleware (executed bottom-up) ββββββββββββββββββββββββββββββββββββββββββ
app.state.limiter = limiter
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(RequestContextMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=api_settings.cors_origins_list, # explicit allowlist, never "*"
allow_credentials=False, # bearer tokens, not cookies
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
expose_headers=["X-Request-ID"],
max_age=600,
)
register_exception_handlers(app)
# ββ Routers β one per resource group βββββββββββββββββββββββββββββββββββββββββ
app.include_router(meta.router)
app.include_router(auth.router)
app.include_router(openalex.router)
app.include_router(documents.router)
app.include_router(chat.router)
app.include_router(search.router)
@app.get("/")
def root() -> dict[str, str | None]:
return {
"name": api_settings.api_title,
"version": api_settings.api_version,
"docs": api_settings.docs_url,
"health": "/api/health",
}
|