Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make FunctionTools Serializable (Declarative) #5052

Merged
merged 14 commits into from
Jan 24, 2025
Prev Previous commit
Next Next commit
move tests, format fixes
victordibia committed Jan 15, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit e97129ed26b8fe6e068394ab7805dbf6f0376841
Original file line number Diff line number Diff line change
@@ -94,59 +94,3 @@ async def test_termination_declarative() -> None:
# Test loading complex composition
loaded_composite = ComponentLoader.load_component(composite_config)
assert isinstance(loaded_composite, AndTerminationCondition)


@pytest.mark.asyncio
async def test_function_tool() -> None:
"""Test FunctionTool with different function types and features."""

# Test sync and async functions
def sync_func(x: int, y: str) -> str:
return y * x

async def async_func(x: float, y: float, cancellation_token: CancellationToken) -> float:
if cancellation_token.is_cancelled():
raise Exception("Cancelled")
return x + y

# Create tools with different configurations
sync_tool = FunctionTool(
func=sync_func, description="Multiply string", global_imports=[ImportFromModule("typing", ("Dict",))]
)
async_tool = FunctionTool(
func=async_func,
description="Add numbers",
name="custom_adder",
global_imports=[ImportFromModule("autogen_core", ("CancellationToken",))],
)

# Test serialization and config

sync_config = sync_tool.dump_component()
assert isinstance(sync_config, ComponentModel)
assert sync_config.config["name"] == "sync_func"
assert len(sync_config.config["global_imports"]) == 1
assert not sync_config.config["has_cancellation_support"]

async_config = async_tool.dump_component()
assert async_config.config["name"] == "custom_adder"
assert async_config.config["has_cancellation_support"]

# Test deserialization and execution
loaded_sync = FunctionTool.load_component(sync_config, FunctionTool)
loaded_async = FunctionTool.load_component(async_config, FunctionTool)

# Test execution and validation
token = CancellationToken()
assert await loaded_sync.run_json({"x": 2, "y": "test"}, token) == "testtest"
assert await loaded_async.run_json({"x": 1.5, "y": 2.5}, token) == 4.0

# Test error cases
with pytest.raises(ValueError):
# Type error
await loaded_sync.run_json({"x": "invalid", "y": "test"}, token)

cancelled_token = CancellationToken()
cancelled_token.cancel()
with pytest.raises(Exception, match="Cancelled"):
await loaded_async.run_json({"x": 1.0, "y": 2.0}, cancelled_token)
60 changes: 59 additions & 1 deletion python/packages/autogen-core/tests/test_component_config.py
Original file line number Diff line number Diff line change
@@ -4,12 +4,14 @@
from typing import Any, Dict

import pytest
from autogen_core import Component, ComponentBase, ComponentLoader, ComponentModel
from autogen_core import Component, ComponentBase, ComponentLoader, ComponentModel, CancellationToken
from autogen_core._component_config import _type_to_provider_str # type: ignore
from autogen_core.models import ChatCompletionClient
from autogen_test_utils import MyInnerComponent, MyOuterComponent
from pydantic import BaseModel, ValidationError
from typing_extensions import Self
from autogen_core.tools import FunctionTool
from autogen_core.code_executor import ImportFromModule


class MyConfig(BaseModel):
@@ -283,3 +285,59 @@ def test_component_version_from_dict() -> None:
assert comp.info == "test"
assert comp.__class__ == ComponentNonOneVersionWithUpgrade
assert comp.dump_component().version == 2


@pytest.mark.asyncio
async def test_function_tool() -> None:
"""Test FunctionTool with different function types and features."""

# Test sync and async functions
def sync_func(x: int, y: str) -> str:
return y * x

async def async_func(x: float, y: float, cancellation_token: CancellationToken) -> float:
if cancellation_token.is_cancelled():
raise Exception("Cancelled")
return x + y

# Create tools with different configurations
sync_tool = FunctionTool(
func=sync_func, description="Multiply string", global_imports=[ImportFromModule("typing", ("Dict",))]
)
async_tool = FunctionTool(
func=async_func,
description="Add numbers",
name="custom_adder",
global_imports=[ImportFromModule("autogen_core", ("CancellationToken",))],
)

# Test serialization and config

sync_config = sync_tool.dump_component()
assert isinstance(sync_config, ComponentModel)
assert sync_config.config["name"] == "sync_func"
assert len(sync_config.config["global_imports"]) == 1
assert not sync_config.config["has_cancellation_support"]

async_config = async_tool.dump_component()
assert async_config.config["name"] == "custom_adder"
assert async_config.config["has_cancellation_support"]

# Test deserialization and execution
loaded_sync = FunctionTool.load_component(sync_config, FunctionTool)
loaded_async = FunctionTool.load_component(async_config, FunctionTool)

# Test execution and validation
token = CancellationToken()
assert await loaded_sync.run_json({"x": 2, "y": "test"}, token) == "testtest"
assert await loaded_async.run_json({"x": 1.5, "y": 2.5}, token) == 4.0

# Test error cases
with pytest.raises(ValueError):
# Type error
await loaded_sync.run_json({"x": "invalid", "y": "test"}, token)

cancelled_token = CancellationToken()
cancelled_token.cancel()
with pytest.raises(Exception, match="Cancelled"):
await loaded_async.run_json({"x": 1.0, "y": 2.0}, cancelled_token)

Unchanged files with check annotations Beta

import_code = import_to_str(import_stmt)
try:
exec(import_code, exec_globals)
except ModuleNotFoundError as e:
raise ModuleNotFoundError(

Check warning on line 141 in python/packages/autogen-core/src/autogen_core/tools/_function_tool.py

Codecov / codecov/patch

python/packages/autogen-core/src/autogen_core/tools/_function_tool.py#L140-L141

Added lines #L140 - L141 were not covered by tests
f"Failed to import {import_code}: Module not found. Please ensure the module is installed."
) from e
except ImportError as e:
raise ImportError(f"Failed to import {import_code}: {str(e)}") from e
except Exception as e:
raise RuntimeError(f"Unexpected error while importing {import_code}: {str(e)}") from e

Check warning on line 147 in python/packages/autogen-core/src/autogen_core/tools/_function_tool.py

Codecov / codecov/patch

python/packages/autogen-core/src/autogen_core/tools/_function_tool.py#L144-L147

Added lines #L144 - L147 were not covered by tests
# Execute function code
try:
exec(config.source_code, exec_globals)
func_name = config.source_code.split("def ")[1].split("(")[0]
except Exception as e:
raise ValueError(f"Could not compile and load function: {e}") from e

Check warning on line 154 in python/packages/autogen-core/src/autogen_core/tools/_function_tool.py

Codecov / codecov/patch

python/packages/autogen-core/src/autogen_core/tools/_function_tool.py#L153-L154

Added lines #L153 - L154 were not covered by tests
# Get function and verify it's callable
func: Callable[..., Any] = exec_globals[func_name]
if not callable(func):
raise TypeError(f"Expected function but got {type(func)}")

Check warning on line 159 in python/packages/autogen-core/src/autogen_core/tools/_function_tool.py

Codecov / codecov/patch

python/packages/autogen-core/src/autogen_core/tools/_function_tool.py#L159

Added line #L159 was not covered by tests
return cls(func, "", None)