SOFTWARE PROJECT · 2026
A chat agent that classifies, decomposes, and streams its own reasoning
I built a two-part chat application: a FastAPI and LangGraph backend that plans how to answer a question, and a React and TypeScript frontend that renders the answer as it streams in. Questions that mix several intents are split into sub-queries and routed through parallel graph branches instead of one long call.
- Backend
- FastAPI + LangGraph
- Frontend
- React + TypeScript + Vite
- Model
- Anthropic Claude
- Transport
- NDJSON streaming
THE QUESTION
One question can hide several intents
A user question can be a quick factual lookup, a request for structured analysis, or a mix of both stitched into one sentence. I wanted the backend to recognize that split automatically, answer each part with the node built for it, and still return one coherent response, rather than forcing every request through a single generic answering path.
I also wanted the UI to stay responsive during a multi-step answer, so the client shows progress as the graph works instead of a blank screen until the final text arrives.
METHOD
Classify, decompose, then route
- 01
Classify and decompose
A query analyzer node classifies the question as insights or analytical, and breaks compound questions into up to three sub-queries.
- 02
Route through the graph
Single-purpose questions go to one LangGraph node. Mixed questions are routed through parallel branches so each sub-query reaches the node built to answer it.
- 03
Optional web search
When a request sets
web_search=true, a search step runs before the answering node, giving the model outside context for that turn. - 04
Synthesize
A summary node assembles the branch outputs into one final response before it is streamed back to the client.
STREAMING AND MEMORY
NDJSON events and compressed history
The chat endpoint returns application/x-ndjson: one JSON object per line, typed as progress, text, error, or final_response. The React frontend consumes this stream incrementally, so partial answer text and status updates appear as the graph produces them rather than all at once.
Conversation history is saved to disk per session and reloaded by conversation ID, which is what makes a session resumable. Once a conversation passes 20 messages, older turns are compressed into a summary and the 10 most recent messages are kept in full.
SCOPE
What this project is, and is not
This is not a retrieval-augmented system
There is no vector database, embedding index, or document chunk retrieval in the current backend. The only path to outside information is the optional web search step, which is scoped per request rather than always on.
REPRODUCE
Running both halves locally
pip install -r requirements.txt
python run.pyThe backend listens on port 8000 by default. Point the frontend's API base URL there and allow its origin under URL_ALLOWED_ORIGINS.