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.

4
Route Groups
3
DB Models
6
Services
4
SSE Event Types

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 ChoiceWhat Was ChosenWhat Was RejectedWhy
Streaming protocolServer-Sent Events (SSE)WebSocketsUnidirectional server-to-client; SSE is simpler with FastAPI StreamingResponse and auto-reconnects
Graph constructionDynamic from React Flow JSONHardcoded pipelineWeb UI allows arbitrary topologies; users can add/remove agents and rewire connections
DatabaseSQLite via SQLAlchemyPostgreSQL / in-memoryZero infrastructure for local dev; SQLAlchemy makes migration to PostgreSQL trivial
Data accessRepository patternDirect ORM queries in routesDecouples 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 / FunctionFilePurpose
app (FastAPI)main.pyFastAPI application with CORS middleware and router registration
run()routes/hedge_fund.pyPOST /hedge-fund/run — SSE streaming endpoint for agent execution
event_generator()routes/hedge_fund.pyAsync generator that yields SSE events and handles client disconnection
create_graph()services/graph.pyBuilds a StateGraph from React Flow nodes and edges
extract_base_agent_key()services/graph.pyStrips random 6-char suffixes from React Flow node IDs to match ANALYST_CONFIG
BacktestServiceservices/backtest_service.pyRuns multi-day backtests with Portfolio Tracking & Performance Metrics" href="../../backtesting_doc/portfolio/index.html">portfolio tracking and performance metrics
ApiKeyServiceservices/api_key_service.pyManages encrypted API key storage and retrieval from the database
HedgeFundFlowdatabase/models.pySQLAlchemy model storing React Flow configs (nodes, edges, viewport, data)
HedgeFundFlowRundatabase/models.pyTracks individual execution runs with status, portfolio snapshots, and results
HedgeFundFlowRunCycledatabase/models.pyIndividual analysis cycles within continuous trading sessions
FlowRepositoryrepositories/flow_repository.pyCRUD operations for HedgeFundFlow with search and template filtering
HedgeFundRequestmodels/schemas.pyPydantic schema for /run requests (tickers, graph nodes/edges, model config)
GraphNode / GraphEdgemodels/schemas.pyPydantic models mirroring React Flow node and edge structures
StartEvent / ProgressUpdateEvent / CompleteEvent / ErrorEventmodels/events.pySSE event types with to_sse() serialization methods

Data Structures

TypeNameDescription
SQLAlchemyHedgeFundFlowColumns: id, name, description, nodes (JSON), edges (JSON), viewport (JSON), data (JSON), is_template, tags
SQLAlchemyHedgeFundFlowRunColumns: id, flow_id (FK), status, trading_mode, request_data (JSON), initial_portfolio, final_portfolio, results
SQLAlchemyHedgeFundFlowRunCycleColumns: id, flow_run_id (FK), cycle_number, analyst_signals, trading_decisions, portfolio_snapshot
EnumFlowRunStatusIDLE, IN_PROGRESS, COMPLETE, ERROR
PydanticGraphNodeid, type, data (dict), position (dict) — mirrors React Flow node shape
PydanticGraphEdgeid, source, target, type, data (dict) — mirrors React Flow edge shape

Interactions

DirectionPartnerInterfaceDescription
←React FrontendPOST /hedge-fund/runReceives graph definition + config, returns SSE stream of progress events
→LangGraph Runtimegraph.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 DatabaseSQLAlchemy ORMPersists flow configs, run history, and API keys
→src/utils/progressprogress.register_handler()Registers a callback that converts progress updates into SSE events
→Ollama (local)HTTP RESTOllamaService checks availability and manages local model downloads
←React FrontendGET/POST /flowsCRUD endpoints for saving and loading flow configurations

Configuration

Knob / ParameterDefaultDescription
CORS originslocalhost:5173Allowed frontend origins for cross-origin requests
trading_mode"one-time"Run mode: one-time, continuous, or advisory
scheduleNoneFor continuous mode: hourly, daily, weekly
is_templateFalseMark a flow as a reusable template
margin_requirement0.0Margin 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.Queue bridge: 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. Each yield sends one SSE frame to the browser.

Edge Cases & Boundaries

Edge CaseWhat HappensWhere in CodeRisk Level
Client disconnects mid-executiondisconnect_task completes; run_task is cancelled; resources freedevent_generatorLow
Unknown agent node ID in React Flowextract_base_agent_key() strips suffix; if base key not in ANALYST_CONFIG, node is silently skippedcreate_graph:L55Medium
Agent raises exception during executionErrorEvent yielded via SSE; error stored in FlowRun.error_messageevent_generatorMedium
No API keys configuredApiKeyService returns empty dict; get_model() raises ValueError caught by error handlerrun:L6High
Multiple portfolio managers in graphEach gets a unique risk manager with suffix matching; handled by risk_manager_nodes mappingcreate_graph:L60Low

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": {...}, ...}