-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Python: preserve explicit null arguments in auto function calling #7108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,10 @@ | |
| ) | ||
| from agent_framework._middleware import FunctionInvocationContext | ||
| from agent_framework._tools import ( | ||
| _auto_invoke_function, | ||
| _parse_annotation, | ||
| _parse_inputs, | ||
| normalize_function_invocation_configuration, | ||
| ) | ||
| from agent_framework.observability import OtelAttr | ||
|
|
||
|
|
@@ -151,6 +153,59 @@ def search(query: str) -> str: | |
| await search.invoke(arguments={}) | ||
|
|
||
|
|
||
| async def test_invoke_preserves_explicit_null_argument(): | ||
| """A required nullable argument the model sets to null must reach the function. | ||
|
|
||
| Regression for #5934: exclude_none dropped the explicit null, so the required | ||
| ``unit`` went missing and the invocation failed. | ||
|
Comment on lines
+156
to
+160
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in the follow-up: |
||
| """ | ||
|
|
||
| @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 isinstance(result, list) | ||
| assert result[0].text == "Seattle:None" | ||
|
|
||
|
|
||
| async def test_invoke_omitted_optional_uses_function_default(): | ||
| """An omitted optional argument still falls back to the function's own default.""" | ||
|
|
||
| @tool | ||
| def get_weather(location: str, unit: str = "C") -> str: | ||
| return f"{location}:{unit}" | ||
|
|
||
| result = await get_weather.invoke(arguments={"location": "Seattle"}) | ||
| assert result[0].text == "Seattle:C" | ||
|
|
||
|
|
||
| async def test_auto_invoke_preserves_explicit_null_argument(): | ||
| """The auto function-calling path must preserve an explicit null argument too. | ||
|
|
||
| Regression for #5934: ``FunctionTool.invoke`` was fixed, but ``_auto_invoke_function`` | ||
| (the path a model's ``function_call`` actually takes) still ran ``exclude_none`` and | ||
| dropped the required ``unit``, so the invocation failed with a missing argument. | ||
| """ | ||
|
|
||
| @tool | ||
| def get_weather(location: str, unit: Literal["C", "F"] | None) -> str: | ||
| return f"{location}:{unit}" | ||
|
|
||
| function_call = Content.from_function_call( | ||
| call_id="call-1", | ||
| name=get_weather.name, | ||
| arguments='{"location": "Seattle", "unit": null}', | ||
| ) | ||
| result = await _auto_invoke_function( | ||
| function_call, | ||
| config=normalize_function_invocation_configuration(None), | ||
| tool_map={get_weather.name: get_weather}, | ||
| ) | ||
| assert result.type == "function_result" | ||
| assert result.result == "Seattle:None" | ||
|
|
||
|
|
||
| async def test_tool_decorator_with_json_schema_invoke_invalid_type(): | ||
| """Test schema type checks run for mapping arguments.""" | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, and you're right that this was the path that mattered. I've pushed a follow-up that switches
_auto_invoke_functiontoexclude_unset=Trueas well, so an explicit null in a model-emittedfunction_callnow reaches the tool instead of being dropped byexclude_none. The three sites (invoke's mapping and BaseModel branches plus the auto-calling path) are consistent now.