Skip to content
Closed
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
6 changes: 3 additions & 3 deletions python/packages/core/agent_framework/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ async def invoke(
parsed_arguments = dict(arguments)
if self.input_model is not None and not self._schema_supplied:
parsed_arguments = self.input_model.model_validate(parsed_arguments).model_dump(
exclude_none=True
exclude_unset=True
)
elif isinstance(arguments, BaseModel):
if (
Expand All @@ -645,7 +645,7 @@ async def invoke(
and not isinstance(arguments, self.input_model)
):
raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}")
parsed_arguments = arguments.model_dump(exclude_none=True)
parsed_arguments = arguments.model_dump(exclude_unset=True)
else:
raise TypeError(
f"Expected mapping-like arguments for tool '{self.name}', got {type(arguments).__name__}"
Expand Down Expand Up @@ -1492,7 +1492,7 @@ async def _auto_invoke_function(
runtime_kwargs["session"] = invocation_session
try:
if not cast(bool, getattr(tool, "_schema_supplied", False)) and tool.input_model is not None:
args = tool.input_model.model_validate(parsed_args).model_dump(exclude_none=True)
args = tool.input_model.model_validate(parsed_args).model_dump(exclude_unset=True)
else:
args = dict(parsed_args)
args = _validate_arguments_against_schema(
Expand Down
31 changes: 31 additions & 0 deletions python/packages/core/tests/core/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
from agent_framework._middleware import FunctionInvocationContext
from agent_framework._tools import (
_auto_invoke_function,
_parse_annotation,
_parse_inputs,
_tools_to_dict,
Expand Down Expand Up @@ -183,6 +184,36 @@ def search(query: str, max_results: int = 10) -> str:
await search.invoke(arguments={"query": "hello", "max_results": "three"})


async def test_tool_invoke_preserves_required_null_argument():
@tool
def get_weather(location: str, unit: Literal["C", "F"] | None) -> str:
return f"{location}:{unit}"

result = await get_weather.invoke(arguments={"location": "Seattle", "unit": None})

assert result[0].text == "Seattle:None"


async def test_auto_function_call_preserves_required_null_argument():
@tool
def get_weather(location: str, unit: Literal["C", "F"] | None) -> str:
return f"{location}:{unit}"

result = await _auto_invoke_function(
Content.from_function_call(
call_id="call_1",
name="get_weather",
arguments='{"location": "Seattle", "unit": null}',
),
config={},
tool_map={"get_weather": get_weather},
)

assert result.type == "function_result"
assert result.result == "Seattle:None"
assert result.exception is None


def test_tool_decorator_with_json_schema_preserves_custom_properties():
"""Test schema passthrough keeps custom JSON schema properties."""

Expand Down