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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import functools
import inspect
import warnings
from textwrap import dedent
from typing import Any, Callable, Sequence
Expand Down Expand Up @@ -103,31 +104,33 @@ def __init__(
super().__init__(args_model, return_type, func_name, description, strict)

async def run(self, args: BaseModel, cancellation_token: CancellationToken) -> Any:
kwargs = {}
# Positional-only parameters cannot be passed by keyword, so collect them
# separately and pass them positionally, in signature order.
positional_args: list[Any] = []
kwargs: dict[str, Any] = {}

for name, parameter in self._signature.parameters.items():
if name == "cancellation_token":
value: Any = cancellation_token
elif hasattr(args, name):
value = getattr(args, name)
else:
continue

for name in self._signature.parameters.keys():
if hasattr(args, name):
kwargs[name] = getattr(args, name)
if parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
positional_args.append(value)
else:
kwargs[name] = value

if asyncio.iscoroutinefunction(self._func):
if self._has_cancellation_support:
result = await self._func(**kwargs, cancellation_token=cancellation_token)
else:
result = await self._func(**kwargs)
result = await self._func(*positional_args, **kwargs)
else:
if self._has_cancellation_support:
result = await asyncio.get_event_loop().run_in_executor(
None,
functools.partial(
self._func,
**kwargs,
cancellation_token=cancellation_token,
),
)
else:
future = asyncio.get_event_loop().run_in_executor(None, functools.partial(self._func, **kwargs))
future = asyncio.get_event_loop().run_in_executor(
None, functools.partial(self._func, *positional_args, **kwargs)
)
if not self._has_cancellation_support:
cancellation_token.link_future(future)
result = await future
result = await future

return result

Expand Down
56 changes: 56 additions & 0 deletions python/packages/autogen-core/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,3 +589,59 @@ async def test_func_tool_with_dataclass_conversion_failure() -> None:

with pytest.raises(ValidationError, match="Field required"):
await tool.run_json(test_input, CancellationToken())


@pytest.mark.asyncio
async def test_func_tool_with_positional_only_parameters() -> None:
def scale(value: int, /, *, factor: int = 2) -> int:
return value * factor

tool = FunctionTool(scale, description="Scale a value.")

assert set(tool.schema["parameters"]["properties"].keys()) == {"value", "factor"}
assert await tool.run_json({"value": 3}, CancellationToken()) == 6
assert await tool.run_json({"value": 3, "factor": 5}, CancellationToken()) == 15


@pytest.mark.asyncio
async def test_async_func_tool_with_positional_only_parameters() -> None:
async def scale(value: int, /, *, factor: int = 2) -> int:
return value * factor

tool = FunctionTool(scale, description="Scale a value.")

assert await tool.run_json({"value": 4}, CancellationToken()) == 8
assert await tool.run_json({"value": 4, "factor": 3}, CancellationToken()) == 12


@pytest.mark.asyncio
async def test_func_tool_with_positional_only_default_value() -> None:
def identity(value: int = 7, /) -> int:
return value

tool = FunctionTool(identity, description="Identity.")

assert await tool.run_json({}, CancellationToken()) == 7
assert await tool.run_json({"value": 9}, CancellationToken()) == 9


@pytest.mark.asyncio
async def test_func_tool_with_positional_only_cancellation_token() -> None:
def record(value: int, cancellation_token: CancellationToken, /) -> int:
return value

tool = FunctionTool(record, description="Record a value.")

assert set(tool.schema["parameters"]["properties"].keys()) == {"value"}
assert await tool.run_json({"value": 5}, CancellationToken()) == 5


@pytest.mark.asyncio
async def test_func_tool_with_mixed_positional_only_and_keyword_arguments() -> None:
def combine(value: int, /, label: str, factor: int = 2) -> str:
return f"{label}:{value * factor}"

tool = FunctionTool(combine, description="Combine values.")

result = await tool.run_json({"value": 3, "label": "total"}, CancellationToken())
assert result == "total:6"