webapp/backend/ — FastAPI REST Backend
REST API with SSE streaming, database persistence, and agent pipeline orchestration
The backend translates HTTP requests into agent pipeline executions. Its most interesting pattern is SSE streaming — when a user clicks “Run” in the web UI, the backend creates a LangGraph from the visual flow definition, executes it, and streams progress events back to the browser in real time.
What is this & why does it exist?
The backend translates HTTP requests into agent pipeline executions. When a user clicks “Run” in the web UI, the backend receives the React Flow Orchestration" href="../../core_doc/graph/index.html">graph definition (nodes + edges), builds a LangGraph StateGraph from it using create_graph(), compiles and invokes it, and streams progress events (Start → Progress → Complete/Error) back to the browser via Server-Sent Events. The repository pattern abstracts SQLite access for flow persistence, and Alembic handles schema migrations.
Real-world analogy
The backend is an air traffic control tower. The frontend is the radar screen (visual display), but the tower (backend) actually coordinates takeoffs and landings (agent executions), maintains flight records (database), and broadcasts status updates to all screens (SSE streaming). The create_graph() function reads the flight plan (React Flow definition) and creates the actual route (LangGraph). The repository is the flight log book — every flow configuration and run result is recorded for replay and audit.
Why Is It Designed This Way?
SSE was chosen over WebSockets because the communication is unidirectional (server → client) and SSE is simpler to implement with FastAPI’s StreamingResponse. The create_graph() function dynamically builds a StateGraph from React Flow node/edge definitions rather than hardcoding the pipeline because the web UI allows users to compose arbitrary agent topologies. The repository pattern decouples database access from route handlers, making it easy to swap SQLite for PostgreSQL. The extract_base_agent_key() function strips random suffixes from React Flow node IDs to match against the canonical ANALYST_CONFIG dictionary.
| Design Choice | What Was Chosen | What Was Rejected | Why |
|---|---|---|---|
| Streaming protocol | Server-Sent Events (SSE) | WebSockets | Unidirectional server-to-client; SSE is simpler with FastAPI StreamingResponse and auto-reconnects |
| Graph construction | Dynamic from React Flow JSON | Hardcoded pipeline | Web UI allows arbitrary topologies; users can add/remove agents and rewire connections |
| Database | SQLite via SQLAlchemy | PostgreSQL / in-memory | Zero infrastructure for local dev; SQLAlchemy makes migration to PostgreSQL trivial |
| Data access | Repository pattern | Direct ORM queries in routes | Decouples persistence from HTTP layer; testable without spinning up a server |
Contents
Architecture
The backend follows a layered architecture: routes → services → repositories → database. The SSE streaming crosses all layers as an async generator.
flowchart TD
subgraph Frontend
UI["React Flow UI"]
end
subgraph Routes
HF["/hedge-fund/run"]
BT["/hedge-fund/backtest"]
FL["/flows"]
SETTINGS["/settings"]
end
subgraph Services
GS["GraphService\n(create_graph)"]
BS["BacktestService"]
AKS["ApiKeyService"]
OLS["OllamaService"]
end
subgraph Repositories
FR["FlowRepository"]
FRR["FlowRunRepository"]
end
subgraph Database
DB["SQLite\n(HedgeFundFlow,\nHedgeFundFlowRun,\nHedgeFundFlowRunCycle)"]
end
UI -->|"POST /hedge-fund/run\n(nodes, edges, tickers)"| HF
HF -->|"SSE: Start, Progress,\nComplete, Error"| UI
HF --> GS
GS -->|"StateGraph"| HF
HF --> FR
BT --> BS
FL --> FR
FR --> DB
FRR --> DB
SETTINGS --> AKS
SSE Streaming Data Flow
The most distinctive pattern: a single HTTP POST triggers an async generator that yields SSE events as agents complete their work.
sequenceDiagram
participant Browser
participant Route as /hedge-fund/run
participant Graph as GraphService
participant LG as LangGraph Runtime
participant Progress as progress tracker
Browser->>Route: POST {nodes, edges, tickers, model}
Route->>Graph: create_graph(nodes, edges)
Graph-->>Route: compiled StateGraph
Route-->>Browser: SSE: StartEvent
Route->>LG: graph.invoke(state) [async task]
loop Agent Execution
LG->>Progress: update_status(agent, ticker, status)
Progress->>Route: progress_handler callback
Route-->>Browser: SSE: ProgressUpdateEvent
end
LG-->>Route: final_state
Route-->>Browser: SSE: CompleteEvent {decisions, signals}
Key Classes & Functions
| Class / Function | File | Purpose |
|---|---|---|
app (FastAPI) | main.py | FastAPI application with CORS middleware and router registration |
run() | routes/hedge_fund.py | POST /hedge-fund/run — SSE streaming endpoint for agent execution |
event_generator() | routes/hedge_fund.py | Async generator that yields SSE events and handles client disconnection |
create_graph() | services/graph.py | Builds a StateGraph from React Flow nodes and edges |
extract_base_agent_key() | services/graph.py | Strips random 6-char suffixes from React Flow node IDs to match ANALYST_CONFIG |
BacktestService | services/backtest_service.py | Runs multi-day backtests with Portfolio Tracking & Performance Metrics" href="../../backtesting_doc/portfolio/index.html">portfolio tracking and performance metrics |
ApiKeyService | services/api_key_service.py | Manages encrypted API key storage and retrieval from the database |
HedgeFundFlow | database/models.py | SQLAlchemy model storing React Flow configs (nodes, edges, viewport, data) |
HedgeFundFlowRun | database/models.py | Tracks individual execution runs with status, portfolio snapshots, and results |
HedgeFundFlowRunCycle | database/models.py | Individual analysis cycles within continuous trading sessions |
FlowRepository | repositories/flow_repository.py | CRUD operations for HedgeFundFlow with search and template filtering |
HedgeFundRequest | models/schemas.py | Pydantic schema for /run requests (tickers, graph nodes/edges, model config) |
GraphNode / GraphEdge | models/schemas.py | Pydantic models mirroring React Flow node and edge structures |
StartEvent / ProgressUpdateEvent / CompleteEvent / ErrorEvent | models/events.py | SSE event types with to_sse() serialization methods |
Data Structures
| Type | Name | Description |
|---|---|---|
| SQLAlchemy | HedgeFundFlow | Columns: id, name, description, nodes (JSON), edges (JSON), viewport (JSON), data (JSON), is_template, tags |
| SQLAlchemy | HedgeFundFlowRun | Columns: id, flow_id (FK), status, trading_mode, request_data (JSON), initial_portfolio, final_portfolio, results |
| SQLAlchemy | HedgeFundFlowRunCycle | Columns: id, flow_run_id (FK), cycle_number, analyst_signals, trading_decisions, portfolio_snapshot |
| Enum | FlowRunStatus | IDLE, IN_PROGRESS, COMPLETE, ERROR |
| Pydantic | GraphNode | id, type, data (dict), position (dict) — mirrors React Flow node shape |
| Pydantic | GraphEdge | id, source, target, type, data (dict) — mirrors React Flow edge shape |
Interactions
| Direction | Partner | Interface | Description |
|---|---|---|---|
| ← | React Frontend | POST /hedge-fund/run | Receives graph definition + config, returns SSE stream of progress events |
| → | LangGraph Runtime | graph.compile().invoke() | Executes the agent pipeline built from React Flow nodes |
| → | src/agents/* | ANALYST_CONFIG[key]["agent_func"] | Dynamically wires agent functions from the core module into the graph |
| → | SQLite Database | SQLAlchemy ORM | Persists flow configs, run history, and API keys |
| → | src/utils/progress | progress.register_handler() | Registers a callback that converts progress updates into SSE events |
| → | Ollama (local) | HTTP REST | OllamaService checks availability and manages local model downloads |
| ← | React Frontend | GET/POST /flows | CRUD endpoints for saving and loading flow configurations |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
| CORS origins | localhost:5173 | Allowed frontend origins for cross-origin requests |
trading_mode | "one-time" | Run mode: one-time, continuous, or advisory |
schedule | None | For continuous mode: hourly, daily, weekly |
is_template | False | Mark a flow as a reusable template |
margin_requirement | 0.0 | Margin requirement ratio for short positions |
Directory Map
Source tree for backend
app/backend/
main.py ← FastAPI app setup, CORS, router registration, startup event
routes/
hedge_fund.py ← /hedge-fund/run (SSE streaming), /hedge-fund/backtest
flows.py ← CRUD endpoints for flow configurations
settings.py ← API key management and Ollama settings
services/
graph.py ← create_graph() builds StateGraph from React Flow JSON
backtest_service.py ← Multi-day backtest execution with performance tracking
api_key_service.py ← Encrypted API key storage and retrieval
agent_service.py ← Agent function wrapper for per-node model configs
portfolio.py ← Portfolio creation from request params
ollama_service.py ← Ollama health checks and model management
database/
connection.py ← SQLAlchemy engine and session factory
models.py ← ORM models: HedgeFundFlow, FlowRun, FlowRunCycle
repositories/
flow_repository.py ← CRUD for HedgeFundFlow with search and template filtering
flow_run_repository.py ← CRUD for FlowRun with status tracking
models/
schemas.py ← Pydantic request/response schemas
events.py ← SSE event classes (Start, Progress, Complete, Error)
Annotated Code Walkthrough
The most important code in this sub-module, annotated with WHY each piece exists:
SSE Streaming Endpoint (routes/hedge_fund.py)
async def run(request_data: HedgeFundRequest, request: Request,
db: Session = Depends(get_db)):
# WHY: Hydrate API keys from DB when frontend doesn't send them
if not request_data.api_keys:
api_key_service = ApiKeyService(db)
request_data.api_keys = api_key_service.get_api_keys_dict()
portfolio = create_portfolio(request_data.initial_cash, ...)
# WHY: Build graph from React Flow nodes/edges - not hardcoded
graph = create_graph(
graph_nodes=request_data.graph_nodes,
graph_edges=request_data.graph_edges
)
graph = graph.compile()
async def event_generator():
progress_queue = asyncio.Queue()
# WHY: Progress handler bridges sync agent code to async SSE stream
def progress_handler(agent_name, ticker, status, analysis, timestamp):
event = ProgressUpdateEvent(agent=agent_name, ticker=ticker,
status=status, timestamp=timestamp)
progress_queue.put_nowait(event)
progress.register_handler(progress_handler)
# WHY: Run graph in background task so we can yield SSE events concurrently
run_task = asyncio.create_task(run_graph_async(graph=graph, ...))
disconnect_task = asyncio.create_task(wait_for_disconnect())
yield StartEvent().to_sse()
while not run_task.done():
# WHY: Detect client disconnect to cancel wasted work
if disconnect_task.done():
run_task.cancel()
return
try:
event = await asyncio.wait_for(progress_queue.get(), timeout=1.0)
yield event.to_sse()
except asyncio.TimeoutError:
pass # Keep polling
result = await run_task
yield CompleteEvent(decisions=result["decisions"], ...).to_sse()
return StreamingResponse(event_generator(), media_type="text/event-stream")
Line-by-line:
asyncio.Queuebridge: Agent code is synchronous (LangGraph invokes agent functions in threads). The progress handler is a sync callback that pushes events into an async queue, bridging the two worlds.wait_for_disconnect(): A concurrent task that monitors the HTTP connection. If the user closes the browser tab, this task completes, and the generator cancels the expensive LLM execution.asyncio.wait_for(queue.get(), timeout=1.0): Polls the queue with a 1-second timeout. This prevents the generator from blocking indefinitely when no progress events are available, allowing disconnect detection to work.StreamingResponse: FastAPI’s built-in SSE support. Eachyieldsends one SSE frame to the browser.
Edge Cases & Boundaries
| Edge Case | What Happens | Where in Code | Risk Level |
|---|---|---|---|
| Client disconnects mid-execution | disconnect_task completes; run_task is cancelled; resources freed | event_generator | Low |
| Unknown agent node ID in React Flow | extract_base_agent_key() strips suffix; if base key not in ANALYST_CONFIG, node is silently skipped | create_graph:L55 | Medium |
| Agent raises exception during execution | ErrorEvent yielded via SSE; error stored in FlowRun.error_message | event_generator | Medium |
| No API keys configured | ApiKeyService returns empty dict; get_model() raises ValueError caught by error handler | run:L6 | High |
| Multiple portfolio managers in graph | Each gets a unique risk manager with suffix matching; handled by risk_manager_nodes mapping | create_graph:L60 | Low |
Code Examples
Example: Calling the run endpoint from curl
curl -N -X POST http://localhost:8000/hedge-fund/run \
-H "Content-Type: application/json" \
-d '{
"tickers": ["AAPL", "MSFT"],
"start_date": "2025-01-01",
"end_date": "2025-06-30",
"initial_cash": 100000,
"model_name": "gpt-4.1",
"model_provider": "OpenAI",
"graph_nodes": [{"id": "warren_buffett_abc123"}],
"graph_edges": []
}'
# Returns SSE stream:
# data: {"type": "start", ...}
# data: {"type": "progress", "agent": "warren_buffett", ...}
# data: {"type": "complete", "decisions": {...}, ...}