diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 93722a8987..1605c5443d 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_none=False ) 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_none=False) 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_none=False) 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..df161a056d 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +import asyncio from typing import Annotated, Any, Literal, get_args, get_origin from unittest.mock import Mock @@ -1462,4 +1463,20 @@ def test_skip_parsing_is_singleton() -> None: assert repr(SKIP_PARSING) == "SKIP_PARSING" +def test_invoke_preserves_explicit_none_arguments() -> None: + """Optional parameters explicitly set to None must not be stripped before invocation.""" + + @tool + def greet(name: str, greeting: str | None = None) -> str: + return f"{greeting or 'Hello'}, {name}!" + + result = asyncio.run(greet.invoke(arguments={"name": "World", "greeting": None})) + assert isinstance(result, list) + assert result[0].text == "Hello, World!" + + result = asyncio.run(greet.invoke(arguments={"name": "World"})) + assert isinstance(result, list) + assert result[0].text == "Hello, World!" + + # endregion