import os
import sqlite3
import json
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from dotenv import load_dotenv
load_dotenv()
from src.agents.orchestrator import FinancialAgentOrchestrator
orchestrator: FinancialAgentOrchestrator = None
DB_PATH = "data/processed/filings.db"
UI_HTML = r"""
20 companies
Recommended ▾
Apple Inc.
Technology / Consumer Electronics
NASDAQ
FY2021-23
SEC 10-K
Sections
4
Revenue · Risk · MD&A · Overview
Fiscal Years
3
2021 · 2022 · 2023
Index
100K+
Document chunks
Ask the Intelligence Agent
Revenue trends 2021–2023
Key risk factors
Stock volatility
Compare with peers
MD&A summary
Cybersecurity risks
Ctrl+Enter to run
Stock Price Trends
Historical OHLCV data · 2021–2023
All
2Y
1Y
Risk Factor Analysis
Cross-company risk intelligence from SEC 10-K filings
Cybersecurity
Regulatory
Supply Chain
Macro / Inflation
Geopolitical
Talent / HR
Analyzing…
Query Log
All agent runs with quality scores and source counts
Search Filings
Hybrid BM25 + vector search across 100K+ document chunks
Evaluation Report
Recall@K · Precision@K · Answer quality metrics
"""
@asynccontextmanager
async def lifespan(app: FastAPI):
global orchestrator
orchestrator = FinancialAgentOrchestrator()
orchestrator.build_index()
yield
app = FastAPI(
title="Financial Intelligence Agent",
description="Multi-agent RAG system — 100K+ financial document chunks",
version="2.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class QueryRequest(BaseModel):
query: str
class QueryResponse(BaseModel):
query: str
answer: str
sources: list
quality_score: float
doc_count: int
blocked: bool = False
class SearchRequest(BaseModel):
query: str
ticker: str = ""
section: str = ""
@app.get("/", response_class=HTMLResponse)
async def root():
return UI_HTML
@app.post("/query", response_model=QueryResponse)
async def query_endpoint(req: QueryRequest):
if not req.query.strip():
raise HTTPException(status_code=400, detail="Query cannot be empty")
result = orchestrator.answer(req.query)
return QueryResponse(
query=result["query"],
answer=result["answer"],
sources=result.get("sources", []),
quality_score=result.get("quality_score", 0.0),
doc_count=result.get("doc_count", 0),
blocked=result.get("blocked", False),
)
@app.post("/search")
async def search_endpoint(req: SearchRequest):
filters = {}
if req.ticker:
filters["ticker"] = [req.ticker]
if req.section:
filters["section"] = req.section
docs = orchestrator.retriever.hybrid_search(req.query, k=10, filters=filters)
results = [
{
"ticker": d["metadata"].get("ticker", ""),
"section": d["metadata"].get("section", ""),
"year": d["metadata"].get("fiscal_year", ""),
"text": d["text"][:300],
"score": d.get("rerank_score", d.get("score", 0)),
}
for d in docs
]
return {"results": results}
@app.get("/prices/{ticker}")
async def prices_endpoint(ticker: str):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT date, close FROM stock_prices WHERE ticker=? ORDER BY date ASC",
(ticker.upper(),),
)
rows = c.fetchall()
conn.close()
if not rows:
return {"dates": [], "closes": []}
return {
"ticker": ticker,
"dates": [r[0] for r in rows],
"closes": [r[1] for r in rows],
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/evaluate")
async def evaluate_endpoint():
from pathlib import Path
report = Path("evaluation/results/eval_report.json")
if report.exists():
return json.loads(report.read_text())
try:
from src.evaluation.eval import run_evaluation
run_evaluation()
if report.exists():
return json.loads(report.read_text())
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=404, detail="Run: python -m src.evaluation.eval")
@app.get("/health")
async def health():
return {"status": "ok", "version": "2.0.0"}