Python: filter non-assistant messages from workflow agent responses - #8729
Manohar Paturi (ManoharPaturi) wants to merge 5 commits into
Conversation
When running workflows converted to an agent via workflow.as_agent(), WorkflowAgent._convert_workflow_events_to_agent_response and _convert_workflow_event_to_agent_response_updates forwarded messages without checking their role. When underlying executors or orchestrations such as GroupChat yield conversation history containing user inputs, system instructions, or tool outputs, these non-assistant messages were re-emitted as caller-facing agent responses and streaming chunks. This change filters out non-assistant messages across AgentResponse, list[Message], Message, and AgentResponseUpdate workflow outputs so that WorkflowAgent exclusively returns assistant output. If an event contains only non-assistant messages, it is safely excluded from agent output without altering internal workflow execution. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
| assistant_messages = [msg for msg in chat_messages if msg.role == "assistant"] | ||
| if assistant_messages: | ||
| messages.extend(assistant_messages) | ||
| raw_representations.append(data) |
There was a problem hiding this comment.
Manohar Paturi (@ManoharPaturi) This filters AgentResponse.messages, but raw_representation still retains the original unfiltered list. A workflow output containing user/system/tool messages therefore continues exposing them through the public non-streaming result, unlike the streaming path. Could this retain only the filtered assistant payloads, or omit the raw representation for this branch?
the list[Message] branch appended the whole yielded list as the raw_representation even when non-assistant entries were filtered out of the public messages, so user/system/tool content stayed reachable through the non-streaming AgentResponse payload. when the list is filtered, append each assistant message's own raw_representation instead; unfiltered lists keep the original object. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
|
Eduard van Valkenburg (@eavanvalkenburg) fixed. the list[Message] branch now appends each assistant message's own raw_representation when the list is filtered, so user/system/tool content no longer leaks through the non-streaming payload; unfiltered lists keep the original object, matching the streaming path. added an assertion to the mixed-list test pinning that no filtered-out role survives in raw_representation. workflow agent suite 64/64 green. |
| # are intentionally excluded. System prompts and tool results are | ||
| # internal workflow artifacts; user messages would be re-emitted | ||
| # (e.g., from GroupChat orchestrators that include full conversation history). | ||
| assistant_messages = [msg for msg in data.messages if msg.role == "assistant"] |
There was a problem hiding this comment.
Manohar Paturi (@ManoharPaturi) Filtering solely by role can leave an invalid tool-call transcript. For assistant(function_call) → tool(function_result) → assistant(final), this keeps the function call and final answer but drops the required result; if WorkflowAgent uses a session, the filtered response can be persisted and replayed to providers that reject orphaned calls. Could this preserve a valid call/result history while exposing only user-facing assistant output, or drop assistant messages that contain only internal function-call content?
Role-only filtering kept assistant(function_call) while dropping its tool(function_result), producing an invalid call-without-result transcript that providers reject on replay when the response is persisted through a session. Assistant messages whose contents are entirely function-call envelopes are now dropped alongside the non-assistant messages, so the public response carries only user-facing assistant output with no dangling calls. Regression test covers call → tool result → final. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
|
Eduard van Valkenburg (@eavanvalkenburg) fixed in 4266443 — took the drop direction you offered: an assistant message whose contents are entirely function-call envelopes is now dropped alongside the non-assistant messages, so call → tool(result) → final keeps only the final and never leaves an orphaned call in the persisted response. the alternative (preserving paired call+result history) would reintroduce exactly the tool-role leakage this PR exists to remove, since the results are tool-role by construction — but happy to switch if you'd rather expose the pair. regression test builds your exact transcript (assistant(get_weather call) → tool(18C result) → assistant(final)) and asserts no function_call content survives while the final text does. workflow agent suite 65/65. |
Motivation & Context
When converting a workflow to an agent using workflow.as_agent(), WorkflowAgent wraps workflow execution inside the agent contract. In multi-agent orchestrations such as GroupChat or workflows where executors yield conversation history, the emitted output often contains user questions, system instructions, or tool results alongside assistant responses.
Previously, WorkflowAgent._convert_workflow_events_to_agent_response and _convert_workflow_event_to_agent_response_updates forwarded messages from AgentResponse, list[Message], and Message payloads without inspecting message roles. This caused user prompts to be re-emitted as part of the agent response, compounding across conversation turns in streaming and non-streaming modes.
Filtering to assistant-only messages ensures that callers invoking the workflow as an agent receive solely the generated assistant output.
Description & Review Guide
Major changes:
In python/packages/core/agent_framework/_workflows/_agent.py, updated _convert_workflow_events_to_agent_response so that only assistant-role messages from AgentResponse, list[Message], and Message payloads are added to the returned AgentResponse. Also updated _convert_workflow_event_to_agent_response_updates to drop non-assistant messages and updates during streaming conversion.
Impact of changes:
WorkflowAgent now returns strictly assistant output. If a workflow output event consists only of non-assistant messages, it is cleanly omitted without crashing or appending dangling raw representations. Internal workflow messaging and inter-executor communication remain completely unchanged.
Review focus:
Please verify the role filtering logic across the AgentResponse, list[Message], and Message branches in _convert_workflow_events_to_agent_response and _convert_workflow_event_to_agent_response_updates, as well as the unit tests covering multi-turn conversation non-compounding and streaming behavior.
Related Issue
Fixes #4261
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.