webapp — Full-Stack Web Application
Full-Stack Web Application
Visual workflow builder with FastAPI backend and React frontend. Build agent pipelines via drag-and-drop, run them with real-time SSE streaming, and manage saved flows and execution history.
🎯 Why This Module Exists
The CLI is powerful for developers but inaccessible to non-technical users and lacks persistence. The web application adds three critical capabilities: (1) Visual workflow building — drag-and-drop agent composition using React Flow, where each agent is a node and edges define execution order; (2) Persistent configuration — saved flows and execution history in SQLite via SQLAlchemy ORM, so users can iterate on strategies without re-entering parameters; and (3) Real-time feedback — Server-Sent Events (SSE) streaming shows agent-by-agent progress as the pipeline executes, with each agent's status, ticker, and analysis pushed to the browser as it completes. Without the web app, users would need to edit Python code to change agent configurations, pass complex CLI arguments for every run, and every execution would be ephemeral with no history.
💡 Real-world analogy
If the CLI is like programming a synthesizer by typing MIDI commands into a terminal, the web app is like GarageBand — a visual interface where you drag instrument tracks (agents) onto a timeline (workflow), connect them with wires (edges), press play, and watch each track light up in real time as it plays (SSE streaming). You can save your compositions (flows) and replay them later, and the mixing console (backtest dashboard) shows you how the ensemble performed.
React Flow SSE Streaming FastAPI + SQLite
Architecture
flowchart LR
subgraph Frontend["Frontend (React + TypeScript)"]
RF["React Flow\nCanvas"]
Hooks["Custom Hooks\n(12 hooks)"]
Services["API Services\nfetch + SSE"]
end
subgraph Backend["Backend (FastAPI + Python)"]
Routes["Routes\n/hedge-fund, /backtest\n/flows, /settings"]
Svc["Services\nGraphService\nBacktestService"]
DB["SQLAlchemy ORM\nSQLite"]
end
subgraph Core["Core (src/)"]
Agents["Agent Functions\n(analysts)"]
Graph["StateGraph\nWorkflow"]
end
RF --> Hooks
Hooks --> Services
Services -->|"HTTP + SSE"| Routes
Routes --> Svc
Svc --> DB
Svc --> Graph
Graph --> Agents
Data Flow
sequenceDiagram
participant User as Browser
participant FE as React Frontend
participant API as FastAPI Backend
participant GS as GraphService
participant Core as src/ Agents
User->>FE: Drag agents, connect edges, click Run
FE->>API: POST /hedge-fund/run (nodes, edges, tickers, model)
API->>GS: create_graph(nodes, edges)
GS->>GS: Build StateGraph from React Flow structure
GS->>Core: graph.invoke(AgentState)
loop For each agent
Core-->>API: progress.update_status(agent, ticker, analysis)
API-->>FE: SSE event (ProgressUpdate)
FE-->>User: Agent card lights up with status
end
Core-->>GS: Final portfolio decision
GS-->>API: Result dict
API-->>FE: SSE event (CompleteEvent)
FE-->>User: Display trading recommendations
🚶 Step-by-Step Narrated Walkthrough
Follow a single user running a hedge fund workflow through the web UI from start to finish:
- Canvas Setup — The user drags Warren Buffett and Ben Graham agent nodes onto the React Flow canvas, then connects both to a Portfolio Tracking & Performance Metrics" href="../backtesting_doc/portfolio/index.html">Portfolio Manager node with edges.
- Configuration — The user selects tickers (AAPL, GOOGL), date range, initial cash ($100k), and chooses GPT-4o as the model. These are captured by custom React hooks.
- Request Dispatch — Clicking "Run" triggers a POST to
/hedge-fund/runwith aHedgeFundRequestpayload containinggraph_nodes,graph_edges, tickers, and model config. - API Key Hydration — The route handler checks if API keys were provided in the request. If not, it loads them from the database via
ApiKeyService. - Orchestration" href="../core_doc/graph/index.html">Graph Construction —
create_graph()inservices/graph.pymaps React Flow node IDs to agent functions usingANALYST_CONFIG. It strips 6-character suffixes (extract_base_agent_key) to match unique canvas IDs to base agent names. - Edge Wiring — The service iterates edges, detects direct analyst-to-portfolio-manager connections, and automatically interposes a Risk Manager node. Entry-point agents (no incoming edges) are connected to
start_node. - SSE Stream Opens — The route returns a
StreamingResponse. A progress handler is registered that pushesProgressUpdateEventobjects to anasyncio.Queue. - Async Execution —
run_graph_async()wraps the synchronousgraph.invoke()inloop.run_in_executor()so it runs in a thread pool without blocking the event loop. - Progress Streaming — As each agent completes, it calls
progress.update_status(). The registered handler enqueues aProgressUpdateEvent. The event generator dequeues it and yields an SSE-formatted string to the browser. - Client Disconnect Detection — A
wait_for_disconnect()coroutine monitors forhttp.disconnectmessages, allowing cleanup if the user navigates away mid-run. - Completion — After all agents finish,
parse_hedge_fund_response()extracts the final JSON. ACompleteEventis pushed to the SSE stream with trading recommendations. - Frontend Update — The React app parses each SSE event, updates agent card states (pending, running, complete), and renders the final portfolio allocation table.
How webapp Connects to core
The web application is a thin orchestration layer on top of the core infrastructure. It does not duplicate any agent logic — instead, it imports AgentState, StateGraph, and agent functions directly from src/. The boundary is clean: the backend constructs a graph from React Flow data, invokes it through the same graph.invoke() path as the CLI, and wraps the output in SSE events.
flowchart TD
subgraph WebApp["webapp (app/)"]
Routes["FastAPI Routes"]
GraphSvc["GraphService\ncreate_graph()"]
BacktestSvc["BacktestService"]
end
subgraph Core["core (src/)"]
AgentState["AgentState"]
Agents["Agent Functions"]
Start["start() entry"]
AnalystConfig["ANALYST_CONFIG"]
end
GraphSvc -->|"imports"| AgentState
GraphSvc -->|"imports"| Agents
GraphSvc -->|"imports"| Start
GraphSvc -->|"imports"| AnalystConfig
Routes -->|"calls"| GraphSvc
BacktestSvc -->|"calls"| GraphSvc
Key insight: webapp owns the HTTP layer, SSE streaming, persistence, and visual workflow construction. core owns the agent logic, state management, and LLM/data infrastructure. The boundary is the StateGraph — webapp builds it from UI data, core executes it.
🏗️ Design Decisions
| Decision | Choice Made | Alternatives Considered | Why This Approach |
|---|---|---|---|
| Real-time updates | Server-Sent Events (SSE) | WebSockets, polling, long-polling | SSE is unidirectional (server to client), matching the progress-update pattern perfectly. Simpler than WebSocket (no handshake, auto-reconnect). Works with standard HTTP infrastructure without special proxy config. |
| Graph construction from UI | Map React Flow nodes/edges to StateGraph at request time |
Pre-compiled graph templates, serialized graph config | Dynamic construction allows arbitrary agent topologies. The extract_base_agent_key function strips canvas-unique suffixes, enabling multiple instances of the same agent type in one workflow. |
| Risk manager interposition | Auto-insert risk manager between analysts and portfolio manager | Require explicit risk manager nodes, skip risk management | Enforces the risk management step without burdening the UI. Users connect analysts to portfolio managers directly; the backend transparently routes through risk assessment first. |
| Async execution | run_in_executor wrapping synchronous graph.invoke() |
Native async agents, Celery task queue | The core agents are synchronous (LangChain/LangGraph). Running in a thread pool avoids blocking the FastAPI event loop while keeping the existing agent code unchanged. No external task queue dependency. |
| Database | SQLite via SQLAlchemy ORM | PostgreSQL, MongoDB, file-based JSON | Zero deployment dependencies. SQLAlchemy provides migration support and type safety. SQLite is sufficient for single-user or small-team usage patterns typical of a trading research tool. |
🔬 Algorithm Spotlight
React Flow to StateGraph Translation
The Problem: The React Flow canvas produces a flat list of nodes (with unique IDs like warren_buffett_a1b2c3) and edges (source/target pairs). This must be translated into a LangGraph StateGraph with proper entry points, agent wiring, and mandatory risk management steps.
The Approach: The create_graph() function performs a multi-pass translation: (1) Extract base agent keys by stripping 6-char suffixes. (2) Register agent nodes, tracking portfolio managers separately. (3) For each portfolio manager, create a paired risk manager with a matching suffix. (4) Scan edges: direct analyst-to-portfolio-manager connections are rerouted through the risk manager. (5) Nodes with no incoming agent edges become entry points connected to start_node. (6) Portfolio managers connect to END.
Why This Algorithm: A single-pass approach would require lookahead to handle the risk manager interposition. The multi-pass design keeps each concern isolated: node registration, edge wiring, and entry/exit point detection are independent passes over the data.
flowchart TD
A["React Flow nodes + edges"] --> B["Pass 1: Extract base agent keys\nstrip 6-char suffixes"]
B --> C["Pass 2: Register agent nodes\nseparate portfolio managers"]
C --> D["Pass 3: Create risk manager\nfor each portfolio manager"]
D --> E["Pass 4: Wire edges\ninterpose risk managers"]
E --> F["Pass 5: Connect entry points\nto start_node"]
F --> G["Pass 6: Connect portfolio\nmanagers to END"]
G --> H["Compiled StateGraph"]
Trace through an example
Given: Canvas has nodes [warren_buffett_a1b2c3, ben_graham_d4e5f6, portfolio_manager_g7h8i9] and edges [buffett->PM, graham->PM].
Pass 1: Extract base keys: warren_buffett, ben_graham, portfolio_manager.
Pass 2: Register warren_buffett_a1b2c3 and ben_graham_d4e5f6 as agent nodes. portfolio_manager_g7h8i9 goes into the portfolio manager set.
Pass 3: Create risk_management_agent_g7h8i9 paired with the portfolio manager.
Pass 4: Both edges target a portfolio manager, so they are rerouted: buffett → risk_manager, graham → risk_manager, risk_manager → portfolio_manager.
Pass 5: Neither analyst has incoming edges, so both connect to start_node.
Result: start_node → [buffett, graham] → risk_manager → portfolio_manager → END
⚠️ What Could Go Wrong
| Failure Mode | Trigger | Detection | Recovery / Mitigation |
|---|---|---|---|
| Client disconnects mid-run | User navigates away or closes the browser tab during execution | wait_for_disconnect() coroutine receives http.disconnect message |
The disconnect task triggers cleanup. However, the graph execution thread continues to completion in the thread pool — results are discarded but resources are eventually freed. |
| Unknown agent node on canvas | React Flow node ID does not match any entry in ANALYST_CONFIG |
extract_base_agent_key returns an unrecognized key; the if base_agent_key not in ANALYST_CONFIG check triggers |
The node is silently skipped during graph construction. The workflow executes with only recognized agents. |
| SSE stream timeout | Agent execution takes too long (e.g., rate-limited API calls with 120s retries) | Browser or reverse proxy closes the SSE connection after idle timeout | The progress queue ensures events are buffered. If the connection drops, the frontend can show the last received state. The backend continues execution regardless. |
| Database migration mismatch | Schema changes between versions | Base.metadata.create_all(bind=engine) at startup |
SQLAlchemy's create_all is additive (creates missing tables) but does not alter existing tables. Breaking schema changes require manual migration. |
| CORS rejection | Frontend served from a URL not in the allowed origins list | Browser blocks the request; no server-side error | Allowed origins are hardcoded to localhost:5173 and 127.0.0.1:5173. Production deployments must update the CORS middleware configuration. |
⚡ Performance Profile
Hot Paths
The most performance-sensitive code paths in the web application:
- Graph construction (
create_graph): O(N + E) where N = nodes, E = edges. Multiple passes over the data but each is linear. Typically under 1ms for realistic graph sizes (5-15 agents). - SSE event serialization: Each
ProgressUpdateEventis serialized to JSON and formatted as an SSE string. Throughput is limited by agent execution speed, not serialization (<1ms per event). - Thread pool dispatch:
run_in_executor(None, ...)uses the default thread pool. Each concurrent run occupies one thread for the duration of graph execution (30s-5min depending on agent count and LLM latency).
Resource Usage
- Memory: Each active run holds a full
AgentStatein memory (~5-20 MB depending on number of tickers and agents). The SQLite database grows linearly with saved flows and execution history. - Concurrency: FastAPI handles concurrent requests via asyncio, but graph execution is thread-bound. The default thread pool (typically 40 threads) limits concurrent workflow runs.
- Startup:
Base.metadata.create_all()runs on startup (~10ms). Ollama status check is async and non-blocking. CORS middleware adds negligible overhead per request.
Sub-Modules
backend/
FastAPI REST API with SSE streaming, SQLAlchemy ORM, and repository pattern.
- Routes:
/hedge-fund,/backtest,/flows,/settings - Services: GraphService, BacktestService, ApiKeyService
- SQLite database with SQLAlchemy models
frontend/
React + TypeScript visual workflow builder with React Flow and 12 custom hooks.
- Drag-and-drop agent canvas with React Flow
- SSE-powered real-time progress display
- Backtest dashboard and flow management
Directory Map
Source tree for webapp
app/
├── backend/ ← FastAPI server
│ ├── main.py ← App factory, CORS, startup events
│ ├── routes/ ← API route handlers
│ │ ├── hedge_fund.py ← /hedge-fund/run (SSE streaming)
│ │ ├── backtest.py ← /backtest endpoint
│ │ ├── flows.py ← CRUD for saved workflows
│ │ └── settings.py ← API key and Ollama management
│ ├── services/ ← Business logic layer
│ │ ├── graph.py ← create_graph(), run_graph_async()
│ │ ├── backtest_service.py ← Backtesting orchestration
│ │ ├── portfolio.py ← Portfolio construction
│ │ └── ollama_service.py ← Local model management
│ ├── database/ ← Persistence layer
│ │ ├── connection.py ← SQLite engine setup
│ │ └── models.py ← SQLAlchemy ORM models
│ └── models/ ← Request/response schemas
│ ├── schemas.py ← Pydantic request models
│ └── events.py ← SSE event types
└── frontend/ ← React application
├── src/
│ ├── components/ ← UI components
│ ├── hooks/ ← 12 custom React hooks
│ ├── services/ ← API client + SSE handler
│ └── App.tsx ← Root component
├── package.json
└── vite.config.ts ← Build configuration