From cf918196256a9cc28354f9f0241f931d2bca68ed Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 22 May 2026 23:30:55 +0800 Subject: [PATCH 1/2] Python: fix Foundry handoff argument serialization (#5861) --- .../_responses.py | 18 ++++++++--- .../foundry_hosting/tests/test_responses.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py index 49a461f9b1..9395fe1c69 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py @@ -9,8 +9,9 @@ import logging import os import tempfile import threading -from collections.abc import AsyncIterable, AsyncIterator, Generator, Mapping, Sequence +from collections.abc import AsyncIterable, AsyncIterator, Generator, Sequence from contextlib import suppress +from dataclasses import asdict, is_dataclass from pathlib import Path from contextlib import AbstractAsyncContextManager, AsyncExitStack, suppress from typing import Protocol, cast @@ -1505,11 +1506,20 @@ def _convert_message_content(content: MessageContent) -> Content: # region Output Item Conversion -def _arguments_to_str(arguments: str | Mapping[str, Any] | None) -> str: +def _argument_json_default(value: Any) -> Any: + if is_dataclass(value) and not isinstance(value, type): + return asdict(value) + to_dict = getattr(value, "to_dict", None) + if callable(to_dict): + return to_dict() + raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") + + +def _arguments_to_str(arguments: Any | None) -> str: """Convert arguments to a JSON string. Args: - arguments: The arguments to convert, can be a string, mapping, or None. + arguments: The arguments to convert, can be a string, JSON-like object, or None. Returns: The arguments as a JSON string. @@ -1518,7 +1528,7 @@ def _arguments_to_str(arguments: str | Mapping[str, Any] | None) -> str: return "" if isinstance(arguments, str): return arguments - return json.dumps(arguments) + return json.dumps(arguments, default=_argument_json_default) async def _to_outputs( diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index 46a3d7f8ef..d5e25b99f9 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -12,6 +12,7 @@ from __future__ import annotations import json from collections.abc import AsyncIterator, Callable +from dataclasses import dataclass from unittest.mock import AsyncMock, MagicMock import httpx @@ -405,6 +406,36 @@ class TestStreaming: assert len(args_done) == 1 assert args_done[0]["data"]["arguments"] == '{"q": "hello"}' + async def test_function_call_streaming_serializes_dataclass_arguments(self) -> None: + @dataclass + class HandoffLikeRequest: + agent_response: AgentResponse + + request = HandoffLikeRequest( + agent_response=AgentResponse( + messages=[Message(role="assistant", contents=[Content.from_text("Need more details")])] + ) + ) + agent = _make_agent( + stream_updates=[ + AgentResponseUpdate( + contents=[Content.from_function_call("call_1", "handoff_to_refund", arguments=request)], + role="assistant", + ), + ] + ) + server = _make_server(agent) + resp = await _post(server, stream=True) + + assert resp.status_code == 200 + events = _parse_sse_events(resp.text) + args_done = [e for e in events if e["event"] == "response.function_call_arguments.done"] + assert len(args_done) == 1 + + payload = json.loads(args_done[0]["data"]["arguments"]) + assert payload["agent_response"]["type"] == "agent_response" + assert payload["agent_response"]["messages"][0]["contents"][0]["text"] == "Need more details" + async def test_alternating_text_and_function_call(self) -> None: agent = _make_agent( stream_updates=[ From 6bc0dc59115a87d4e38b2e551827c566a3d3f7f1 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 22 May 2026 23:31:18 +0800 Subject: [PATCH 2/2] fix: update sequential workflow sample output handling (#5976) --- .../orchestrations/sequential_agents.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/python/samples/03-workflows/orchestrations/sequential_agents.py b/python/samples/03-workflows/orchestrations/sequential_agents.py index 70a25d9f58..62d3aa54f8 100644 --- a/python/samples/03-workflows/orchestrations/sequential_agents.py +++ b/python/samples/03-workflows/orchestrations/sequential_agents.py @@ -4,7 +4,7 @@ import asyncio import os from typing import cast -from agent_framework import Agent, Message +from agent_framework import Agent, AgentResponse, Message from agent_framework.foundry import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential @@ -17,9 +17,9 @@ load_dotenv() Sample: Sequential workflow (agent-focused API) with shared conversation context Build a high-level sequential workflow using SequentialBuilder and two domain agents. -The shared conversation (list[Message]) flows through each participant. Each agent -appends its assistant message to the context. The workflow outputs the final conversation -list when complete. +The shared conversation flows through each participant. Each agent appends its +assistant message to the context. The sample prints the original user message plus +the visible outputs from both agents. Note on internal adapters: - Sequential orchestration includes small adapter nodes for input normalization @@ -56,17 +56,19 @@ async def main() -> None: ) # 2) Build sequential workflow: writer -> reviewer - workflow = SequentialBuilder(participants=[writer, reviewer]).build() + workflow = SequentialBuilder(participants=[writer, reviewer], output_from="all").build() # 3) Run and collect outputs - outputs: list[list[Message]] = [] - async for event in workflow.run("Write a tagline for a budget-friendly eBike.", stream=True): - if event.type == "output": - outputs.append(cast(list[Message], event.data)) + prompt = "Write a tagline for a budget-friendly eBike." + result = await workflow.run(prompt) + conversation = [Message(role="user", contents=[prompt])] + for output in result.get_outputs(): + response = cast(AgentResponse, output) + conversation.extend(response.messages) - if outputs: + if conversation: print("===== Final Conversation =====") - for i, msg in enumerate(outputs[-1], start=1): + for i, msg in enumerate(conversation, start=1): name = msg.author_name or ("assistant" if msg.role == "assistant" else "user") print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")