Skip to content

Python: fix: preserve null tool arguments#6123

Closed
he-yufeng wants to merge 1 commit into
microsoft:mainfrom
he-yufeng:fix/preserve-null-tool-arguments
Closed

Python: fix: preserve null tool arguments#6123
he-yufeng wants to merge 1 commit into
microsoft:mainfrom
he-yufeng:fix/preserve-null-tool-arguments

Conversation

@he-yufeng
Copy link
Copy Markdown
Contributor

Fixes #5934.

This keeps explicitly provided null tool arguments when the inferred Pydantic input model validates the payload. The old model_dump(exclude_none=True) path removed those keys before schema validation and invocation, so a required-but-nullable argument looked missing.

The helper still omits default None fields, and it only restores fields known to the validated model so unknown null keys are not reintroduced after validation.

To verify

  • uv run --no-sync pytest python\packages\core\tests\core\test_tools.py::test_tool_invoke_preserves_required_nullable_argument python\packages\core\tests\core\test_tools.py::test_tool_invoke_does_not_reintroduce_unknown_null_arguments python\packages\core\tests\core\test_function_invocation_logic.py::test_base_client_with_function_calling_preserves_null_arguments -q
  • uv run --no-sync ruff check agent_framework\_tools.py tests\core\test_tools.py tests\core\test_function_invocation_logic.py
  • uv run --no-sync mypy agent_framework\_tools.py
  • python -m py_compile python\packages\core\agent_framework\_tools.py python\packages\core\tests\core\test_tools.py python\packages\core\tests\core\test_function_invocation_logic.py
  • git diff --check

Copilot AI review requested due to automatic review settings May 27, 2026 19:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR fixes tool argument serialization so explicitly provided null/None values for required-but-nullable parameters are preserved, while unknown null arguments are not reintroduced—covering both direct FunctionTool.invoke calls and the auto function-calling flow.

Changes:

  • Added _dump_tool_arguments to preserve explicitly provided None fields when dumping validated Pydantic models.
  • Updated tool invocation and auto-invocation flows to use _dump_tool_arguments instead of model_dump(exclude_none=True).
  • Added tests to validate preservation of nullable required args and dropping of unknown null args, including function-calling scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
python/packages/core/agent_framework/_tools.py Adds helper to dump tool args while preserving explicit None, and wires it into invoke/auto-invoke paths.
python/packages/core/tests/core/test_tools.py Adds tests for direct tool invocation behavior with nullable required args and unknown null args.
python/packages/core/tests/core/test_function_invocation_logic.py Adds test ensuring base client function-calling preserves null arguments through execution.

def _dump_tool_arguments(model: BaseModel, *, include_none_from: Iterable[str] | None = None) -> dict[str, Any]:
arguments = model.model_dump(exclude_none=True)
field_names = type(model).model_fields
for name in include_none_from if include_none_from is not None else model.model_fields_set:
exclude_none=True
parsed_arguments = _dump_tool_arguments(
self.input_model.model_validate(parsed_arguments),
include_none_from=parsed_arguments,
):
raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}")
parsed_arguments = arguments.model_dump(exclude_none=True)
parsed_arguments = _dump_tool_arguments(arguments)
@github-actions github-actions Bot changed the title fix: preserve null tool arguments Python: fix: preserve null tool arguments May 27, 2026

def _dump_tool_arguments(model: BaseModel, *, include_none_from: Iterable[str] | None = None) -> dict[str, Any]:
arguments = model.model_dump(exclude_none=True)
field_names = type(model).model_fields
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens with an aliased field on the mapping path? include_none_from there is the raw caller keys, but field_names / getattr use Python field names. With a custom input_model using Field(alias=...) + populate_by_name, an explicit null sent under the alias key fails name in field_names, so the None is dropped, re-introducing the exact bug this fixes (aliased models only; auto-generated @tool models are unaffected). Could we resolve alias -> field name first? One option, which also folds in the non-str-key guard:

Suggested change
field_names = type(model).model_fields
field_names = type(model).model_fields
alias_to_field = {f.alias: n for n, f in field_names.items() if isinstance(f.alias, str)}
for name in include_none_from if include_none_from is not None else model.model_fields_set:
if not isinstance(name, str):
continue
resolved = alias_to_field.get(name, name)
if resolved in field_names and getattr(model, resolved, None) is None:
arguments[resolved] = None

@moonbox3
Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py10217992%219–220, 404, 406, 419, 444–446, 454, 472, 486, 493, 500, 523, 525, 532, 540, 660, 695–697, 700–702, 704, 710, 762–764, 789, 815, 819, 857–859, 863, 885, 1028–1029, 1033, 1069, 1081, 1083, 1088–1091, 1112, 1116, 1120, 1134–1136, 1483, 1569, 1597, 1619, 1623, 1753, 1757, 1803, 1864–1865, 1968, 2021, 2041, 2043, 2099, 2162, 2334–2335, 2355, 2411–2412, 2550–2551, 2618, 2623, 2630
TOTAL36843433988% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7357 34 💤 0 ❌ 0 🔥 1m 51s ⏱️

@moonbox3
Copy link
Copy Markdown
Contributor

Please make sure you're not duplicating work. #5944 was the first PR introduced with the fix.

@moonbox3 moonbox3 closed this May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Auto function calling removes null arguments

3 participants