From 1cefe3c471c52cdc41b181378a85b2cc5f6dfd07 Mon Sep 17 00:00:00 2001 From: Oxygen56 <100782273+Oxygen56@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:22:35 +0800 Subject: [PATCH] fix(core): serialize structured tool results as JSON --- .../src/autogen_core/tools/_base.py | 10 ++++ .../packages/autogen-core/tests/test_tools.py | 59 ++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/python/packages/autogen-core/src/autogen_core/tools/_base.py b/python/packages/autogen-core/src/autogen_core/tools/_base.py index d2ea76e21da1..4d22b8d5ca52 100644 --- a/python/packages/autogen-core/src/autogen_core/tools/_base.py +++ b/python/packages/autogen-core/src/autogen_core/tools/_base.py @@ -2,6 +2,7 @@ import logging from abc import ABC, abstractmethod from collections.abc import Sequence +from dataclasses import asdict, is_dataclass from typing import ( Any, AsyncGenerator, @@ -171,6 +172,15 @@ def return_value_as_string(self, value: Any) -> str: return json.dumps(dumped) return str(dumped) + if is_dataclass(value) and not isinstance(value, type): + value = asdict(value) + + if isinstance(value, (dict, list)): + try: + return json.dumps(value) + except (TypeError, ValueError): + return str(value) + return str(value) @abstractmethod diff --git a/python/packages/autogen-core/tests/test_tools.py b/python/packages/autogen-core/tests/test_tools.py index c2efed058abf..e35785bc9beb 100644 --- a/python/packages/autogen-core/tests/test_tools.py +++ b/python/packages/autogen-core/tests/test_tools.py @@ -1,15 +1,17 @@ import inspect +import json from dataclasses import dataclass from functools import partial from typing import Annotated, List import pytest +from pydantic import BaseModel, Field, ValidationError, model_serializer +from pydantic_core import PydanticUndefined + from autogen_core import CancellationToken from autogen_core._function_utils import get_typed_signature from autogen_core.tools import BaseTool, FunctionTool from autogen_core.tools._base import ToolSchema -from pydantic import BaseModel, Field, ValidationError, model_serializer -from pydantic_core import PydanticUndefined class MyArgs(BaseModel): @@ -480,6 +482,59 @@ def my_function() -> List[int]: assert tool.return_value_as_string(result) == "[1, 2]" +@pytest.mark.asyncio +async def test_func_tool_return_dict_serializes_to_json() -> None: + def my_function() -> dict[str, object]: + return {"status": "success", "items": ["alpha", "beta"], "count": 2} + + tool = FunctionTool(my_function, description="Function tool.") + result = await tool.run_json({}, CancellationToken()) + assert isinstance(result, dict) + assert json.loads(tool.return_value_as_string(result)) == { + "status": "success", + "items": ["alpha", "beta"], + "count": 2, + } + + +@pytest.mark.asyncio +async def test_func_tool_return_nested_list_serializes_to_json() -> None: + def my_function() -> list[dict[str, int]]: + return [{"count": 1}, {"count": 2}] + + tool = FunctionTool(my_function, description="Function tool.") + result = await tool.run_json({}, CancellationToken()) + assert isinstance(result, list) + assert json.loads(tool.return_value_as_string(result)) == [{"count": 1}, {"count": 2}] + + +@pytest.mark.asyncio +async def test_func_tool_return_dataclass_serializes_to_json() -> None: + @dataclass + class ToolPayload: + status: str + count: int + + def my_function() -> ToolPayload: + return ToolPayload(status="success", count=2) + + tool = FunctionTool(my_function, description="Function tool.") + result = await tool.run_json({}, CancellationToken()) + assert isinstance(result, ToolPayload) + assert json.loads(tool.return_value_as_string(result)) == {"status": "success", "count": 2} + + +@pytest.mark.asyncio +async def test_func_tool_return_non_json_serializable_value_falls_back_to_str() -> None: + def my_function() -> dict[str, object]: + return {"ids": {1, 2}} + + tool = FunctionTool(my_function, description="Function tool.") + result = await tool.run_json({}, CancellationToken()) + assert isinstance(result, dict) + assert tool.return_value_as_string(result) == str(result) + + def test_nested_tool_schema_generation() -> None: schema: ToolSchema = MyNestedTool().schema