FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Python: preserve explicit null arguments in auto function calling (#7… · sricursion/agent-framework@0d29250 · GitHub

Repository navigation

Commit 0d29250

Browse files
Python: preserve explicit null arguments in auto function calling (microsoft#7108)
* Python: preserve explicit null arguments in auto function calling FunctionTool.invoke dumped validated arguments with model_dump(exclude_none=True), which strips any argument the model set to null. A required nullable parameter (e.g. unit: Literal["C","F"] | None) that the model deliberately sets to null was therefore dropped, and the function failed to invoke on the missing argument. Use exclude_unset instead: keep the arguments the model actually provided (null included) and omit only the ones it left out, so the function's own defaults still apply. Because the input model is generated from the function signature, its field defaults match the signature defaults, so omitted optionals are unchanged. Fixes microsoft#5934 Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> * Python: extend null-arg fix to the auto function-calling path The earlier change fixed FunctionTool.invoke, but _auto_invoke_function (the path a model-emitted function_call actually takes) still ran model_dump(exclude_none=True), so an explicit null for a required nullable argument was still dropped and the call failed with a missing argument. Switch it to exclude_unset to match invoke, and add a regression test that drives _auto_invoke_function with an explicit null. --------- Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
1 parent b5e635e commit 0d29250

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

‎python/packages/core/agent_framework/_tools.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -655,8 +655,14 @@ async def invoke(
655655
if isinstance(arguments, Mapping):
656656
parsed_arguments = dict(arguments)
657657
if self.input_model is not None and not self._schema_supplied:
658+
# exclude_unset (not exclude_none): keep arguments the model
659+
# explicitly provided even when their value is null, and drop
660+
# only the ones it left out, so the function's own defaults
661+
# apply. Excluding null instead would strip a required nullable
662+
# parameter the model deliberately set to null, failing the
663+
# invocation on the missing argument (#5934).
658664
parsed_arguments = self.input_model.model_validate(parsed_arguments).model_dump(
659-
exclude_none=True
665+
exclude_unset=True
660666
)
661667
elif isinstance(arguments, BaseModel):
662668
if (
@@ -665,7 +671,7 @@ async def invoke(
665671
and not isinstance(arguments, self.input_model)
666672
):
667673
raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}")
668-
parsed_arguments = arguments.model_dump(exclude_none=True)
674+
parsed_arguments = arguments.model_dump(exclude_unset=True)
669675
else:
670676
raise TypeError(
671677
f"Expected mapping-like arguments for tool '{self.name}', got {type(arguments).__name__}"
@@ -1480,7 +1486,10 @@ async def _auto_invoke_function(
14801486
runtime_kwargs["session"] = invocation_session
14811487
try:
14821488
if not cast(bool, getattr(tool, "_schema_supplied", False)) and tool.input_model is not None:
1483-
args = tool.input_model.model_validate(parsed_args).model_dump(exclude_none=True)
1489+
# exclude_unset (not exclude_none) so an argument the model explicitly set
1490+
# to null still reaches the function; see FunctionTool.invoke for the full
1491+
# rationale. This is the auto-calling path #5934 actually hits.
1492+
args = tool.input_model.model_validate(parsed_args).model_dump(exclude_unset=True)
14841493
else:
14851494
args = dict(parsed_args)
14861495
args = _validate_arguments_against_schema(

‎python/packages/core/tests/core/test_tools.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
)
1818
from agent_framework._middleware import FunctionInvocationContext
1919
from agent_framework._tools import (
20+
_auto_invoke_function,
2021
_parse_annotation,
2122
_parse_inputs,
23+
normalize_function_invocation_configuration,
2224
)
2325
from agent_framework.observability import OtelAttr
2426

@@ -151,6 +153,59 @@ def search(query: str) -> str:
151153
await search.invoke(arguments={})
152154

153155

156+
async def test_invoke_preserves_explicit_null_argument():
157+
"""A required nullable argument the model sets to null must reach the function.
158+
159+
Regression for #5934: exclude_none dropped the explicit null, so the required
160+
``unit`` went missing and the invocation failed.
161+
"""
162+
163+
@tool
164+
def get_weather(location: str, unit: Literal["C", "F"] | None) -> str:
165+
return f"{location}:{unit}"
166+
167+
result = await get_weather.invoke(arguments={"location": "Seattle", "unit": None})
168+
assert isinstance(result, list)
169+
assert result[0].text == "Seattle:None"
170+
171+
172+
async def test_invoke_omitted_optional_uses_function_default():
173+
"""An omitted optional argument still falls back to the function's own default."""
174+
175+
@tool
176+
def get_weather(location: str, unit: str = "C") -> str:
177+
return f"{location}:{unit}"
178+
179+
result = await get_weather.invoke(arguments={"location": "Seattle"})
180+
assert result[0].text == "Seattle:C"
181+
182+
183+
async def test_auto_invoke_preserves_explicit_null_argument():
184+
"""The auto function-calling path must preserve an explicit null argument too.
185+
186+
Regression for #5934: ``FunctionTool.invoke`` was fixed, but ``_auto_invoke_function``
187+
(the path a model's ``function_call`` actually takes) still ran ``exclude_none`` and
188+
dropped the required ``unit``, so the invocation failed with a missing argument.
189+
"""
190+
191+
@tool
192+
def get_weather(location: str, unit: Literal["C", "F"] | None) -> str:
193+
return f"{location}:{unit}"
194+
195+
function_call = Content.from_function_call(
196+
call_id="call-1",
197+
name=get_weather.name,
198+
arguments='{"location": "Seattle", "unit": null}',
199+
)
200+
result = await _auto_invoke_function(
201+
function_call,
202+
config=normalize_function_invocation_configuration(None),
203+
tool_map={get_weather.name: get_weather},
204+
)
205+
assert result.type == "function_result"
206+
assert result.result == "Seattle:None"
207+
208+
154209
async def test_tool_decorator_with_json_schema_invoke_invalid_type():
155210
"""Test schema type checks run for mapping arguments."""
156211

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL