Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions python/packages/autogen-core/src/autogen_core/tools/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
59 changes: 57 additions & 2 deletions python/packages/autogen-core/tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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

Expand Down