SSE Turns Python Generators into Streaming LLM Chat Replies
Chat interfaces get instant feedback instead of a frozen wait; a single generator wrapped in StreamingResponse turns any async LLM stream into an SSE stream the browser can render incrementally.
Streaming a large model's response to a web page relies on three pieces: a generator that yields partial chunks, an SSE response wrapper, and a frontend that reads frames and appends them to the DOM. In FastAPI, `yield` suspends the function after returning each piece, and `StreamingResponse` keeps reading from that generator and sending each yielded value over the wire. The response must use `media_type="text/event-stream"`, and headers like `Cache-Control: no-cache` and `X-Accel-Buffering: no` prevent proxies from delaying the stream.
The backend reads from an existing `agent.execute_stream(messages)` method with `async for`, handling two event types: `reasoning` fragments go to a thinking area, and `answer` fragments accumulate into the final reply. Each event is JSON-serialized and wrapped in an SSE frame with `event:` and `data:` lines, terminated by a blank line. A final `done` event signals completion; an `error` event carries a user-readable failure message.
On the client, plain `fetch` reads the response body with `getReader()`, decodes with `TextDecoder` (preserving partial multi-byte characters), splits frames on blank lines, and appends content based on the JSON `type` field. An `AbortController` lets the user stop the stream mid-generation, canceling the reader and restoring the send button. The whole pattern works for any generator-based agent, not just this one.
The pattern keeps the generator as the single source of truth: the backend never buffers the full reply, and the frontend never reassembles JSON events, because the abstraction already separates reasoning from answer.
Handling incomplete UTF-8 characters is a real-world detail that only surfaces with Chinese text; the `stream: !done` flag shows why streaming decoders are not optional for non-Latin scripts.
The `X-Accel-Buffering` header is a reminder that a working SSE setup depends on the proxy in front of the app, not just the framework; forgetting it leaves the user staring at a blank page until the stream flushes.