Chat responses aren't actually streamed, full answer arrives in one shot #12
Labels
No labels
Doing
P1: critical
P2: important
P3: useful
P4: nice to have
P5: reminder
To Do
bug
invalid
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
peacelink/ai-assistant#12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Both the public chat widget and the internal webapp show a "…" placeholder for
the entire duration of the final answer generation, then the complete response
appears all at once. There is no real token-by-token streaming anywhere in the
current agent implementation, despite the transport (SSE) and the widget's
client-side code already being built to support it.
Observed behavior
Tested locally via the public widget (
/chat), question: "Quali posizioni haPeaceLink sulla deterrenza nucleare?"
0.5s, final-answer LLM call 24.8s (1,179 output tokens, ≈47 tok/s — a
normal generation rate for Claude Sonnet on a long response, not a hang).
~4,000-character answer appears at once.
Root cause
POST /chat(ai/assistant/api/main.py) streams an SSE response and alreadyplumbs a
partial_callbackall the way through to the agent(
ai/assistant/webapp/agent.py,_async_run→_run_graph), so thetransport layer is not the problem.
The problem is in
_run_graph's event loop(
ai/assistant/webapp/agent.py:293-358), which consumesgraph.astream_events(...)(LangGraph's event stream) and only callspartial_callbackhere:Two issues:
on_chain_endfor the"agent"node — i.e. once an entireLangGraph node (one full, already-completed LLM call) finishes. It is not
token-level streaming; it's a coarse "a step just finished" relay.
contentandtool_callsto be present.The final answer node — the one that actually writes the response the user
reads — by definition has no
tool_calls(there's nothing left to call).So this condition is never true for the final answer, and
partial_callbackis never invoked for it at all.The only content the client ever receives for the final answer is the single
doneevent, sent fromapi/main.pyafteragent._async_run(...)returns —i.e. after the entire LLM generation has already finished server-side.
The widget's own code (
ui/js/chat-widget.js,readSseStream) alreadyhandles this correctly on the receiving end — it renders
payload.textonevery
partialevent and would show growing text if the events existed. Thegap is entirely server-side.
Impact
api/main.py→ sharedagent.py) and internal webapp(
webapp/chat.py→ sameagent.py) are both affected — this is sharedcode, not a widget-only issue.
in full before anything renders. Longer answers feel proportionally worse
since there's no feedback that anything is happening beyond the existing
"…" placeholder.
Proposed fix
Switch the LLM call inside the "agent" LangGraph node from a blocking call to
a streaming one (e.g. LiteLLM/LangChain
.astream()instead of the currentinvoke-style call), and forward each chunk through
partial_callbackas itarrives, instead of only once at node-end.
Scope: this means changing the agent's node implementation in
webapp/agent.py(shared by both consumers), not just the API/widget layer.Both
api/main.py's SSE relay and the widget'sreadSseStreamalreadysupport incremental
partialevents, so no changes should be needed oneither of those once the agent itself actually streams.
Not investigated yet
agent.pythe "agent" node's LLM call is made (separatefrom the
_run_graphevent-loop code referenced above) — needed to knowhow invasive the switch to streaming is.
astream_eventsnaturally surfaceson_chat_model_stream(or similar per-token) events already, which mightmake this simpler than manually switching the underlying LLM call — worth
checking before assuming a bigger rewrite is needed.
that currently assumes a single complete
contentstring per iteration.Chat responses aren't actually streamed — full answer arrives in one shotto Chat responses aren't actually streamed, full answer arrives in one shot