import os from contextlib import asynccontextmanager from dotenv import load_dotenv from fastapi import FastAPI, HTTPException, Request from fastapi.concurrency import run_in_threadpool from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field import langchain from core.config import Settings from core.graph_builder import build_financial_graph from core.runner import create_llm, run_financial_query, astream_financial_query @asynccontextmanager async def lifespan(app: FastAPI): load_dotenv() langchain.debug = os.getenv("LANGCHAIN_DEBUG", "").lower() in ("1", "true", "yes") settings = Settings() llm = create_llm(settings) app.state.settings = settings app.state.graph = build_financial_graph(llm) yield app = FastAPI(title="FinAgent", lifespan=lifespan) class ChatRequest(BaseModel): query: str = Field(..., min_length=1, max_length=16000) class StepOut(BaseModel): node: str content: str step_latency: float | None = None total_latency: float | None = None class ChatResponse(BaseModel): memo: str | None = None steps: list[StepOut] = Field(default_factory=list) total_latency: float | None = None @app.get("/health") def health(): return {"status": "ok"} @app.post("/chat", response_model=ChatResponse) async def chat(request: Request, body: ChatRequest): graph = request.app.state.graph q = body.query.strip() if not q: raise HTTPException(status_code=400, detail="query must not be empty") try: result = await run_in_threadpool(run_financial_query, graph, q) except Exception as e: raise HTTPException(status_code=503, detail=str(e)) from e return ChatResponse(**result) @app.post("/chat/stream") async def chat_stream(request: Request, body: ChatRequest): graph = request.app.state.graph q = body.query.strip() if not q: raise HTTPException(status_code=400, detail="query must not be empty") return StreamingResponse( astream_financial_query(graph, q), media_type="text/event-stream" )