(last updated Aug-2025)
This document captures the full stack that streams a LinkedIn-driven portfolio from a LangGraph agent (FastAPI backend) to a React frontend, including real-time workflow visualisation.
┌─────────────────────────────────────────┐
│ FastAPI │
│ /api/portfolio (POST) │→ synchronous JSON (legacy)
│ /api/portfolio/stream (POST) │→ **SSE** stream
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ LangGraph `portfolio_graph.py` │
│ StateGraph<InputState, OutputState> │
│ ├─ linkedin_node │ fetch & validate profile
│ ├─ about_node │ build about JSON & stream
│ ├─ projects_node │ GPT-4o extraction + stream
│ ├─ experience_node │ transform positions + stream
│ └─ conditional route (missing profile) │
└─────────────────────────────────────────┘
The backend owns two responsibilities:
- Running the agent workflow.
- Translating LangGraph events to Server-Sent Events so the browser can consume them over a single HTTP response.
class InputState(TypedDict):
linkedin_id: strclass OutputState(TypedDict):
about_data: AboutSectionDict | None
projects_data: ProjectsSectionDict | None
experience_data: dict | None
linkedin_status: Literal["found", "not_found"]LangGraph event_type |
Forwarded as | Notes |
|---|---|---|
custom (structured chunk) |
custom | progressive JSON for a node |
custom (node_update) |
custom | `{status:"started" |
values |
values | cumulative OutputState snapshot |
stream_state() in backend/app/agent/tools.py walks any dict/list recursively and emits valid JSON after every incremental mutation – giving the illusion of fine-grained streaming.
flowchart TD
START[__start__]
LINKEDIN[linkedin_node]
ABOUT[about_node]
PROJECTS[projects_node]
EXP[experience_node]
END[__end__]
START --> LINKEDIN
LINKEDIN -->|profile found| ABOUT
LINKEDIN -.->|profile missing| END
ABOUT --> PROJECTS
PROJECTS --> EXP
EXP --> END
(unchanged sections 2-7 – see previous revision)
| Stage | Description / Key Code |
|---|---|
| 1. POST request |
|
| 2. ReadableStream loop |
`reader.read()` chunks decoded with `TextDecoder`. Buffer split on /\n\n/ to isolate SSE records. |
| 3. `handleSSE(msg)` |
|
| 4. Stream close |
Hook sets finished=true, streaming=false, clears loadingSection. |
Because every structured chunk contains only the leaf that changed, we merge using a shallow spread by key ({...prev, about_data: newPartial}); our sections are small so a full recursive merge wasn’t necessary.
EventSource only supports GET. We need POST to send the LinkedIn ID → used the fetch+ReadableStream pattern which works in every modern browser and Node runtime.
- Reconnect / Resume – store
threadIdor current state and resume streams. - Error Boundary – wrap page routes to display neat fallback UI.
- Optimistic Skeletons – add ShadCN skeleton placeholders for smoother UX.
- Graph editing – drive
ReactFlowfrom the same LangGraph JSON so the diagram is generated instead of hard-coded.
© 2025 Streaming Portfolio Demo