finrag / backend /app /main.py
fardeen16's picture
Upload FinRAG source for Render Docker build
e6c104f verified
Raw History Blame Contribute Delete
3.34 kB
"""FastAPI surface for the agent.
`AGENT_MODE=mock` (default) replays canned traces so the UI can be developed
without a GPU. `AGENT_MODE=live` runs the LangGraph supervisor. The event
contract is the same either way; only the generator behind StreamingResponse
changes.
"""
from __future__ import annotations
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
from typing import AsyncIterator
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
from .config import get_settings
from .events import ChatRequest
from .mock_agent import replay
from .stream import live_stream
SSE_HEADERS = {
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
}
def _warmup() -> None:
"""Load embedding + reranker weights and open stores before the first query.
The first Librarian call otherwise spends ~15s downloading/loading models,
which on Fargate is paid by whoever happens to ask first.
"""
from .agent.embeddings import get_embeddings
from .agent.resources import get_qdrant, get_reranker, get_sql_database
get_embeddings()
get_reranker()
get_qdrant()
get_sql_database()
@asynccontextmanager
async def lifespan(_app: FastAPI):
settings = get_settings()
if settings.agent_mode == "live" and settings.warmup_on_start:
await asyncio.to_thread(_warmup)
yield
app = FastAPI(title="FinRAG API", version="0.2.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
async def health() -> dict[str, object]:
settings = get_settings()
return {
"status": "ok",
"agent": settings.agent_mode,
"vllm": settings.vllm_base_url,
"embeddings": settings.embedding_provider,
"qdrant": str(settings.qdrant_url or settings.qdrant_path),
}
@app.post("/api/chat/stream")
async def chat_stream(
body: ChatRequest,
speed: float = Query(1.0, gt=0, le=50, description="Mock replay speed. Ignored in live mode."),
) -> StreamingResponse:
"""Stream agent state and answer tokens as Server-Sent Events.
POST rather than GET because the query goes in the body and, once auth
lands, the JWT goes in an Authorization header. That rules out `EventSource`
on the client — it is GET-only and cannot set headers.
"""
settings = get_settings()
async def stream() -> AsyncIterator[bytes]:
frames = (
live_stream(body.query, body.conversation_id)
if settings.agent_mode == "live"
else replay(body.query, body.conversation_id, speed)
)
async for frame in frames:
yield frame.encode("utf-8")
return StreamingResponse(stream(), media_type="text/event-stream", headers=SSE_HEADERS)
_dist = Path(__file__).resolve().parents[2] / "frontend" / "dist"
if _dist.is_dir():
app.mount("/", StaticFiles(directory=str(_dist), html=True), name="spa")