| from contextlib import asynccontextmanager |
| import time |
| from pathlib import Path |
|
|
| from fastapi import FastAPI, HTTPException, Request, Response |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import FileResponse |
| from fastapi.staticfiles import StaticFiles |
|
|
| from server.retriever import get_retriever, get_vectorstore, has_documents |
| from server.bm25_index import build_from_vectorstore, DEFAULT_WORKSPACE as BM25_DEFAULT |
| from server.reranker import load_reranker |
| from server.memory import create_memory |
| from server.chain import build_qa_chain |
| from server.utils import configure_logging, setup_logger, log_memory_mb |
|
|
| |
| UPLOAD_DIR = Path(__file__).resolve().parent.parent / "data" / "raw" |
|
|
| configure_logging() |
| logger = setup_logger(__name__) |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| """Initialize chain, memory, retriever on startup.""" |
| logger.info("Starting Prism server...") |
| app.state.memory = create_memory() |
| app.state.retriever = None |
| app.state.chain = None |
| app.state.eval_log = [] |
| app.state.is_contextualizing = False |
| app.state.upload_jobs = {} |
|
|
| |
| load_reranker() |
|
|
| if has_documents(BM25_DEFAULT): |
| vectorstore = get_vectorstore(BM25_DEFAULT) |
| build_from_vectorstore(vectorstore, workspace_id=BM25_DEFAULT) |
| app.state.retriever = get_retriever(BM25_DEFAULT) |
| app.state.chain = build_qa_chain(app.state.retriever, app.state.memory) |
| logger.info("Chain initialized with existing documents") |
| else: |
| logger.info("No documents found - chain will be built after first upload") |
|
|
| log_memory_mb(logger, "startup") |
| logger.info("Prism server ready") |
| yield |
|
|
|
|
| app = FastAPI(title="Prism API", lifespan=lifespan) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origin_regex=r"https://.*\.vercel\.app|http://localhost:\d+", |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
|
|
| @app.middleware("http") |
| async def log_requests(request: Request, call_next) -> Response: |
| start = time.perf_counter() |
| response = await call_next(request) |
| ms = (time.perf_counter() - start) * 1000 |
| logger.info( |
| "%s %s %d %.0fms", |
| request.method, |
| request.url.path, |
| response.status_code, |
| ms, |
| ) |
| return response |
|
|
| |
| from server.routes import chat, eval, upload, workspaces |
|
|
| app.include_router(chat.router, prefix="/api") |
| app.include_router(eval.router, prefix="/api") |
| app.include_router(upload.router, prefix="/api") |
| app.include_router(workspaces.router, prefix="/api") |
|
|
|
|
| @app.get("/health") |
| async def health(): |
| return {"status": "ok", "version": "3.0.0"} |
|
|
|
|
| @app.get("/api/files/{filename}") |
| async def serve_file(filename: str) -> FileResponse: |
| """Serve uploaded files from data/raw/ for citation PDF links.""" |
| upload_root = UPLOAD_DIR.resolve() |
| target = (UPLOAD_DIR / filename).resolve() |
| if not target.is_relative_to(upload_root): |
| raise HTTPException(status_code=400, detail="Invalid filename") |
| if not target.exists(): |
| raise HTTPException(status_code=404, detail="File not found") |
| return FileResponse(str(target)) |
|
|
|
|
| |
| frontend_dist = Path(__file__).resolve().parent.parent / "frontend" / "dist" |
| if frontend_dist.exists(): |
| app.mount("/", StaticFiles(directory=str(frontend_dist), html=True), name="frontend") |
|
|