File size: 3,631 Bytes
f5628ad 2868f80 f5628ad 3a34cb6 f5628ad 3a34cb6 f5628ad 43e87a7 1e5c62a 43e87a7 f5628ad 25ed685 f5628ad 3a34cb6 38ea650 3a34cb6 2868f80 f5628ad 7dac988 f5628ad 25ed685 34ca534 f5628ad 43e87a7 1e5c62a f5628ad 25ed685 7dac988 f5628ad 7dac988 f5628ad 2322e90 f5628ad 2868f80 f5628ad 1e5c62a f5628ad 1e5c62a f5628ad 8446976 7dac988 8446976 3a34cb6 10860d7 3a34cb6 f5628ad | 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 | 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
# Module-level constant so tests can monkeypatch it
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 # True while background contextual refresh runs
app.state.upload_jobs = {} # job_id -> {status, message, briefing, error, workspace}
# Pre-load reranker to avoid cold-start latency on first query
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
# Import and include route modules
from server.routes import chat, eval, upload, workspaces # noqa: E402
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))
# Serve React frontend build if it exists
frontend_dist = Path(__file__).resolve().parent.parent / "frontend" / "dist"
if frontend_dist.exists():
app.mount("/", StaticFiles(directory=str(frontend_dist), html=True), name="frontend")
|