Skip to content

feat(adapters): add streaming support to WatsonxOrchestrateAgent#1505

Open
sundowatch wants to merge 13 commits into
i-am-bee:mainfrom
sundowatch:feat/watsonx-orchestrate-streaming
Open

feat(adapters): add streaming support to WatsonxOrchestrateAgent#1505
sundowatch wants to merge 13 commits into
i-am-bee:mainfrom
sundowatch:feat/watsonx-orchestrate-streaming

Conversation

@sundowatch

Copy link
Copy Markdown
Contributor

Summary

WatsonxOrchestrateAgent always 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 related finish_reason hardcoding in WatsonxOrchestrateServerAgent.run().

Changes

agents/agent.py — streaming support
Replaces the single client.post() call with client.stream() + SSE parsing:

  • Sends stream=True in the request body
  • Iterates aiter_lines() on the streaming response
  • Parses each data: {...} SSE line; stops on [DONE] sentinel
  • Accumulates content deltas and captures finish_reason from the final chunk
  • Assembles a ChatCompletionResponse from the collected data, preserving the existing WatsonxOrchestrateAgentOutput interface — no breaking changes

serve/agent.py — fix hardcoded finish_reason
WatsonxOrchestrateServerAgent.run() was hardcoding finish_reason="stop" for all responses regardless of what the agent actually returned. Now it reads the actual finish_reason from the response's raw field when available (populated by WatsonxOrchestrateAgent after this change), falling back to "stop" for other agent types.

Test plan

  • Syntax and ruff lint pass on both modified files
  • No changes to the public interface (WatsonxOrchestrateAgentOutput, WatsonxOrchestrateAgent constructor)
  • Manual verification requires a WatsonX Orchestrate instance — SSE parsing follows the standard OpenAI-compatible format already used elsewhere in the serve layer

…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>
@sundowatch
sundowatch requested a review from a team as a code owner June 17, 2026 08:45
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 17, 2026
@github-actions github-actions Bot added python Python related functionality and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jun 17, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread python/beeai_framework/adapters/watsonx_orchestrate/agents/agent.py Outdated
Comment thread python/beeai_framework/adapters/watsonx_orchestrate/serve/agent.py Outdated
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 18, 2026
…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>
@sundowatch

Copy link
Copy Markdown
Contributor Author

Added unit tests for the SSE chat-completion parsing in cc027efa: I pulled the accumulation out into a small pure helper and covered multi-chunk content, the [DONE] sentinel, the optional space after data:, and the defensive handling for malformed/unexpected payloads. The goal is to make the streaming path verifiable in CI without a live WatsonX Orchestrate instance. Happy to tweak the approach if you'd prefer it structured differently.

@Tomas2D

Tomas2D commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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. run() collects the entire stream (lines = [line async for line in streaming_response.aiter_lines()]) before parsing, so it still blocks until the full response arrives — the framework's streaming convention is per-token emitter events (e.g. new_token/partial_update), which aren't emitted here. So this is really "consume the SSE endpoint" rather than "add streaming"; consider wiring incremental emitter events or reframing the title/description.

Minor: consider await streaming_response.aread() before raise_for_status() so server error bodies aren't lost, and an integration test for the run() path (only the pure parser is currently tested). The serve/agent.py finish_reason fix looks good.

…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>
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 13, 2026
@sundowatch

Copy link
Copy Markdown
Contributor Author

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 ca8d1a5d:

  • Real incremental streaming: run() now parses each chunk as it arrives and emits per-token update events (WatsonxOrchestrateAgentUpdateEvent carrying the delta and the accumulated content), registered on the agent's emitter — so it follows the framework's emitter-based streaming convention instead of blocking until the full response is buffered. The fully accumulated result is still returned as before.
  • Error bodies: it now reads the body when is_error before raise_for_status(), so the server's error detail is preserved — without consuming the body on the success (streaming) path.
  • Tests: kept the pure per-chunk parser unit tests and added a run()-path integration test with a mocked stream that asserts the incremental update events actually fire per token (not one blob at the end) and that the final content / [DONE] handling is correct — all runnable in CI without a live WatsonX Orchestrate instance.

So the title now genuinely reflects streaming. Happy to rename the update event (I followed the a2a adapter's convention) or adjust anything else you'd prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Python related functionality size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants