From 58e40673d5548908f73d8ff6313d45012d694ff6 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 1 Jun 2026 06:48:56 +0800 Subject: [PATCH] fix(python): preserve null tool arguments --- .../packages/core/agent_framework/_tools.py | 6 ++-- python/packages/core/tests/core/test_tools.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 93722a8987..f702ed0d2c 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -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 ( @@ -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__}" @@ -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( diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index b3762bf4ef..cf5e36d82f 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -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, @@ -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."""