-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Python: Preserve null arguments during tool invocation #5944
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
Changes from all commits
c0fedbb
5d8452c
e87b543
16c34de
9f3f926
a2d6f66
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 |
|---|---|---|
|
|
@@ -198,6 +198,44 @@ def _parse_inputs( # pyright: ignore[reportUnusedFunction] | |
| return parsed_inputs | ||
|
|
||
|
|
||
| def _model_dump_preserving_explicit_none(model: BaseModel) -> dict[str, Any]: | ||
| """Dump a model without dropping fields that were explicitly set to None.""" | ||
| # Pydantic's exclude_none removes both default None values and explicit null arguments. | ||
| # Restore only fields present in model_fields_set so omitted optional fields stay omitted. | ||
| dumped = model.model_dump(exclude_none=True) | ||
|
Member
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. there is also a
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. Good call. |
||
| _restore_explicit_none_fields(model, dumped) | ||
| return dumped | ||
|
moonbox3 marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def _restore_explicit_none_fields(value: Any, dumped: Any) -> None: | ||
| if isinstance(value, BaseModel) and isinstance(dumped, dict): | ||
| # model_fields_set distinguishes an explicitly provided null from a default None. | ||
| for field_name in value.model_fields_set: | ||
| if not isinstance(field_name, str): | ||
| continue | ||
|
|
||
| field_value = getattr(value, field_name, None) | ||
| if field_value is None: | ||
| dumped[field_name] = None | ||
| elif field_name in dumped: | ||
| _restore_explicit_none_fields(field_value, dumped[field_name]) | ||
| return | ||
|
|
||
| if isinstance(value, Mapping) and isinstance(dumped, Mapping): | ||
| value_mapping = cast(Mapping[object, object], value) | ||
| dumped_mapping = cast(Mapping[object, object], dumped) | ||
| for key, item in value_mapping.items(): | ||
| if key in dumped_mapping: | ||
| _restore_explicit_none_fields(item, dumped_mapping[key]) | ||
| return | ||
|
|
||
| if isinstance(value, list | tuple) and isinstance(dumped, list): | ||
| value_sequence = cast(Sequence[object], value) | ||
| dumped_list = cast(list[object], dumped) | ||
| for item, dumped_item in zip(value_sequence, dumped_list): | ||
| _restore_explicit_none_fields(item, dumped_item) | ||
|
|
||
|
|
||
| # region Tools | ||
|
|
||
|
|
||
|
|
@@ -654,8 +692,8 @@ async def invoke( | |
| if isinstance(arguments, Mapping): | ||
| 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 | ||
| parsed_arguments = _model_dump_preserving_explicit_none( | ||
| self.input_model.model_validate(parsed_arguments) | ||
| ) | ||
| elif isinstance(arguments, BaseModel): | ||
| if ( | ||
|
|
@@ -664,7 +702,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 = _model_dump_preserving_explicit_none(arguments) | ||
| else: | ||
| raise TypeError( | ||
| f"Expected mapping-like arguments for tool '{self.name}', got {type(arguments).__name__}" | ||
|
|
@@ -1479,7 +1517,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 = _model_dump_preserving_explicit_none(tool.input_model.model_validate(parsed_args)) | ||
| else: | ||
| args = dict(parsed_args) | ||
| args = _validate_arguments_against_schema( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.