feat(adapters): add streaming support to WatsonxOrchestrateAgent#1505
feat(adapters): add streaming support to WatsonxOrchestrateAgent#1505sundowatch wants to merge 13 commits into
Conversation
…treamToolCallMiddleware The middleware module had zero test coverage. This adds 36 unit tests across two files covering both middleware implementations: - GlobalTrajectoryMiddleware: _create_target helper (all 5 input types), TraceLevel model validation, init defaults, _format_payload for all value types (str/int/bool/None/FrameworkError/dict), pretty-print flag, and two integration-style tests verifying that the enabled flag correctly gates writes to the target. - StreamToolCallMiddleware: init state, unbind cleanup, _process with matching/non-matching tool names, incremental delta tracking, and duplicate-content deduplication. Signed-off-by: ibrahim <ihsanduvac@gmail.com>
WatsonxOrchestrateAgent.run() always issued a non-streaming HTTP request
(stream=False) even though the WatsonX Orchestrate API supports SSE streaming.
This forced the framework to wait for the full response before returning,
increasing latency.
The agent now sends stream=True and consumes the SSE response incrementally:
- Parses each `data: {...}` line from the server-sent event stream
- Accumulates content deltas into a single string
- Captures finish_reason and response metadata from the stream
- Assembles a ChatCompletionResponse from the collected chunks, preserving
the same WatsonxOrchestrateAgentOutput interface with no breaking change
Also fixes the hardcoded finish_reason="stop" in WatsonxOrchestrateServerAgent.run():
when the underlying agent is a WatsonxOrchestrateAgent its raw response now
carries the actual finish_reason, which is forwarded to the WatsonX Orchestrate
server response.
Signed-off-by: ibrahim <ihsanduvac@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces streaming support for the Watsonx Orchestrate agent adapter by transitioning to a streaming client, parsing Server-Sent Events (SSE), and reconstructing the chat completion response. It also updates the serving layer to dynamically extract the finish_reason from raw response choices and adds comprehensive unit tests for StreamToolCallMiddleware and GlobalTrajectoryMiddleware. The review feedback recommends enhancing robustness by handling SSE lines without a space after the data: prefix and adding defensive type checks when parsing JSON chunks and raw response attributes to prevent potential runtime crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…on read Addresses review feedback: - SSE parsing now accepts "data:" with or without the optional space and defensively skips chunks/choices/deltas that are not the expected dict/list shapes, preventing AttributeError if the API returns an unexpected structure. - WatsonxOrchestrateServerAgent.run() verifies that the response's "raw" attribute and its nested choices are a dict/list before reading finish_reason, so serving non-WatsonX agent types cannot crash. Signed-off-by: ibrahim <ihsanduvac@gmail.com>
# Conflicts: # python/tests/middleware/test_stream_tool_call.py # python/tests/middleware/test_trajectory.py
…instance Extract the OpenAI-compatible SSE chat-completion accumulation in WatsonxOrchestrateAgent.run() into a pure _parse_sse_chat_completion() helper (behaviour-preserving: run() already buffers the full stream before returning) and add unit tests for it. The tests cover multi-chunk content accumulation, the [DONE] sentinel, the optional space after 'data:', and the defensive handling for malformed JSON and non-dict chunks/choices/deltas — so the streaming logic is verifiable in CI without a running WatsonX Orchestrate instance. Signed-off-by: ibrahim <ihsanduvac@gmail.com>
|
Added unit tests for the SSE chat-completion parsing in |
|
Thanks @sundowatch — the code is correct, the SSE parser is nicely defensive, and CI is green. One substantive note: this doesn't actually stream to the caller. Minor: consider |
…teAgent Address review: run() previously buffered the whole SSE stream before returning, so it consumed the streaming endpoint but never streamed to the caller. It now parses each chunk as it arrives and emits per-token 'update' events (WatsonxOrchestrateAgentUpdateEvent with the delta + accumulated content), matching the framework's emitter-based streaming convention, while still returning the fully accumulated result. Also read the response body on error before raise_for_status() so the server's error detail isn't lost, and register the new event type on the emitter. Tests: pure per-chunk parser unit tests plus a run()-path integration test (mocked stream) asserting the incremental update events and final content — all verifiable in CI without a live WatsonX Orchestrate instance. Signed-off-by: ibrahim <ihsanduvac@gmail.com>
|
Thanks for the detailed review, @Tomas2D — you're right that the previous version only consumed the SSE endpoint without streaming to the caller. I've reworked it in
So the title now genuinely reflects streaming. Happy to rename the |
Summary
WatsonxOrchestrateAgentalways issued a blocking, non-streaming HTTP request to the WatsonX Orchestrate API (stream=False), even though the API fully supports Server-Sent Events (SSE) streaming. This forced the framework to wait for the entire response before returning anything, adding unnecessary latency.This PR implements streaming for
WatsonxOrchestrateAgent.run()and fixes the relatedfinish_reasonhardcoding inWatsonxOrchestrateServerAgent.run().Changes
agents/agent.py— streaming supportReplaces the single
client.post()call withclient.stream()+ SSE parsing:stream=Truein the request bodyaiter_lines()on the streaming responsedata: {...}SSE line; stops on[DONE]sentinelfinish_reasonfrom the final chunkChatCompletionResponsefrom the collected data, preserving the existingWatsonxOrchestrateAgentOutputinterface — no breaking changesserve/agent.py— fix hardcodedfinish_reasonWatsonxOrchestrateServerAgent.run()was hardcodingfinish_reason="stop"for all responses regardless of what the agent actually returned. Now it reads the actualfinish_reasonfrom the response'srawfield when available (populated byWatsonxOrchestrateAgentafter this change), falling back to"stop"for other agent types.Test plan
WatsonxOrchestrateAgentOutput,WatsonxOrchestrateAgentconstructor)