From 6c9fd468c893cea1f1531b6962bdf61185a0a49f Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 15 May 2026 03:15:21 +0800 Subject: [PATCH] Python: fix Foundry handoff argument serialization --- .../_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 c34c65538c..8ad17803bb 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 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 typing import Protocol, cast @@ -1394,11 +1395,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. @@ -1407,7 +1417,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 e4d545d6d7..8b1c5aa8d0 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -12,6 +12,7 @@ import json from collections.abc import AsyncIterator, Callable +from dataclasses import dataclass from unittest.mock import AsyncMock, MagicMock import httpx @@ -401,6 +402,36 @@ async def test_function_call_streaming(self) -> None: 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=[