From 225d9ceff866958da6d9686d03c984bbe8431498 Mon Sep 17 00:00:00 2001 From: Deeven Seru Date: Tue, 10 Feb 2026 10:27:59 +0000 Subject: [PATCH 1/5] fix(toolbox-core): parse parameter default value from tool schema The ParameterSchema model was missing the default field, causing Pydantic to drop the default value provided in the tool schema. This resulted in the default value not being reflected in the tool's signature. Fixes #506 --- .../toolbox-core/src/toolbox_core/protocol.py | 9 ++++- packages/toolbox-core/tests/test_protocol.py | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index e522d7922..f76cc1b89 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -81,6 +81,7 @@ class ParameterSchema(BaseModel): authSources: Optional[list[str]] = None items: Optional["ParameterSchema"] = None additionalProperties: Optional[Union[bool, AdditionalPropertiesSchema]] = None + default: Optional[Any] = None def __get_type(self) -> Type: base_type: Type @@ -103,11 +104,17 @@ def __get_type(self) -> Type: return base_type def to_param(self) -> Parameter: + default_value = Parameter.empty + if self.default is not None: + default_value = self.default + elif not self.required: + default_value = None + return Parameter( self.name, Parameter.POSITIONAL_OR_KEYWORD, annotation=self.__get_type(), - default=Parameter.empty if self.required else None, + default=default_value, ) diff --git a/packages/toolbox-core/tests/test_protocol.py b/packages/toolbox-core/tests/test_protocol.py index 8dd60e3fb..1e86a02ba 100644 --- a/packages/toolbox-core/tests/test_protocol.py +++ b/packages/toolbox-core/tests/test_protocol.py @@ -289,3 +289,40 @@ def test_parameter_schema_map_unsupported_value_type_error(): expected_error_msg = f"Unsupported schema type: {unsupported_type}" with pytest.raises(ValueError, match=expected_error_msg): schema._ParameterSchema__get_type() + +def test_parameter_schema_with_default(): + """Tests ParameterSchema with a default value provided.""" + schema = ParameterSchema( + name="limit", + type="integer", + description="Limit results", + required=False, + default=10, + ) + expected_type = Optional[int] + + assert schema._ParameterSchema__get_type() == expected_type + + param = schema.to_param() + assert isinstance(param, Parameter) + assert param.name == "limit" + assert param.annotation == expected_type + assert param.default == 10 + + +def test_parameter_schema_required_with_default(): + """Tests ParameterSchema with default value, ignoring required=True implies it is optional in python signature.""" + # Although illogical in some schemas, if default is present, it should be used as default. + schema = ParameterSchema( + name="retry_count", + type="integer", + description="Retries", + required=True, + default=3, + ) + + # get_type still respects required=True for type hint + assert schema._ParameterSchema__get_type() == int + + param = schema.to_param() + assert param.default == 3 From fc2f77c7ad7e524d835990d6ea7707533fbcdf43 Mon Sep 17 00:00:00 2001 From: Deeven Seru Date: Tue, 10 Feb 2026 11:12:23 +0000 Subject: [PATCH 2/5] fix: correctly order parameters with defaults and update pydantic models --- packages/toolbox-core/src/toolbox_core/tool.py | 18 +++++++++++------- .../toolbox-core/src/toolbox_core/utils.py | 6 ++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index ecc3f568a..e868cfbf0 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -15,7 +15,7 @@ import copy import itertools from collections import OrderedDict -from inspect import Signature +from inspect import Parameter, Signature from types import MappingProxyType from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union @@ -86,13 +86,17 @@ def __init__( self.__params = params self.__pydantic_model = params_to_pydantic_model(name, self.__params) - # Separate parameters into required (no default) and optional (with - # default) to prevent the "non-default argument follows default + # Separate parameters into those without a default and those with a + # default to prevent the "non-default argument follows default # argument" error when creating the function signature. - required_params = (p for p in self.__params if p.required) - optional_params = (p for p in self.__params if not p.required) - ordered_params = itertools.chain(required_params, optional_params) - inspect_type_params = [param.to_param() for param in ordered_params] + inspect_type_params = [param.to_param() for param in self.__params] + params_no_default = [ + p for p in inspect_type_params if p.default is Parameter.empty + ] + params_with_default = [ + p for p in inspect_type_params if p.default is not Parameter.empty + ] + inspect_type_params = params_no_default + params_with_default # the following properties are set to help anyone that might inspect it determine usage self.__name__ = name diff --git a/packages/toolbox-core/src/toolbox_core/utils.py b/packages/toolbox-core/src/toolbox_core/utils.py index 1f34c4bef..7c5b9a331 100644 --- a/packages/toolbox-core/src/toolbox_core/utils.py +++ b/packages/toolbox-core/src/toolbox_core/utils.py @@ -120,10 +120,12 @@ def params_to_pydantic_model( field_definitions = {} for field in params: - # Determine the default value based on the 'required' flag. + # Determine the default value based on the 'required' flag and the 'default' field. # '...' (Ellipsis) signifies a required field in Pydantic. - # 'None' makes the field optional with a default value of None. + # If a default value is provided in the schema, it should be used. default_value = ... if field.required else None + if field.default is not None: + default_value = field.default field_definitions[field.name] = cast( Any, From f6023cd2d8947271f902d6b51c49b4ddf120aea4 Mon Sep 17 00:00:00 2001 From: DEVELOPER-DEEVEN <144827577+DEVELOPER-DEEVEN@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:47:44 +0530 Subject: [PATCH 3/5] fix(toolbox-core): honor explicit defaults and clean lint issues --- .../toolbox-core/src/toolbox_core/protocol.py | 7 ++++++- .../toolbox-core/src/toolbox_core/tool.py | 1 - .../toolbox-core/src/toolbox_core/utils.py | 2 +- packages/toolbox-core/tests/test_protocol.py | 15 ++++++++++++++ packages/toolbox-core/tests/test_utils.py | 20 +++++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index f76cc1b89..a47ff86c1 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -83,6 +83,11 @@ class ParameterSchema(BaseModel): additionalProperties: Optional[Union[bool, AdditionalPropertiesSchema]] = None default: Optional[Any] = None + @property + def has_default(self) -> bool: + """Returns True if `default` was explicitly provided in schema input.""" + return "default" in self.model_fields_set + def __get_type(self) -> Type: base_type: Type if self.type == "array": @@ -105,7 +110,7 @@ def __get_type(self) -> Type: def to_param(self) -> Parameter: default_value = Parameter.empty - if self.default is not None: + if self.has_default: default_value = self.default elif not self.required: default_value = None diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index e868cfbf0..03e9cbc85 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -13,7 +13,6 @@ # limitations under the License. import copy -import itertools from collections import OrderedDict from inspect import Parameter, Signature from types import MappingProxyType diff --git a/packages/toolbox-core/src/toolbox_core/utils.py b/packages/toolbox-core/src/toolbox_core/utils.py index 7c5b9a331..00c001572 100644 --- a/packages/toolbox-core/src/toolbox_core/utils.py +++ b/packages/toolbox-core/src/toolbox_core/utils.py @@ -124,7 +124,7 @@ def params_to_pydantic_model( # '...' (Ellipsis) signifies a required field in Pydantic. # If a default value is provided in the schema, it should be used. default_value = ... if field.required else None - if field.default is not None: + if field.has_default: default_value = field.default field_definitions[field.name] = cast( diff --git a/packages/toolbox-core/tests/test_protocol.py b/packages/toolbox-core/tests/test_protocol.py index 1e86a02ba..fe6320cd4 100644 --- a/packages/toolbox-core/tests/test_protocol.py +++ b/packages/toolbox-core/tests/test_protocol.py @@ -290,6 +290,7 @@ def test_parameter_schema_map_unsupported_value_type_error(): with pytest.raises(ValueError, match=expected_error_msg): schema._ParameterSchema__get_type() + def test_parameter_schema_with_default(): """Tests ParameterSchema with a default value provided.""" schema = ParameterSchema( @@ -326,3 +327,17 @@ def test_parameter_schema_required_with_default(): param = schema.to_param() assert param.default == 3 + + +def test_parameter_schema_required_with_explicit_none_default(): + """Tests explicit default=None is treated as a provided default.""" + schema = ParameterSchema( + name="opt_in", + type="boolean", + description="Optional flag", + required=True, + default=None, + ) + + param = schema.to_param() + assert param.default is None diff --git a/packages/toolbox-core/tests/test_utils.py b/packages/toolbox-core/tests/test_utils.py index 1f54ae0dc..ec6ebb22b 100644 --- a/packages/toolbox-core/tests/test_utils.py +++ b/packages/toolbox-core/tests/test_utils.py @@ -37,6 +37,8 @@ def create_param_mock(name: str, description: str, annotation: Type) -> Mock: param_mock.name = name param_mock.description = description param_mock.required = True + param_mock.default = None + param_mock.has_default = False mock_param_info = Mock() mock_param_info.annotation = annotation @@ -424,6 +426,24 @@ def test_params_to_pydantic_model_with_params(): Model(name="Bob", age="thirty", is_active=True) +def test_params_to_pydantic_model_uses_explicit_default_none(): + """Test that explicit default=None is honored for required schema fields.""" + tool_name = "MyToolWithExplicitNoneDefault" + params = [ + ParameterSchema( + name="message", + type="string", + description="Message value", + required=True, + default=None, + ) + ] + Model = params_to_pydantic_model(tool_name, params) + + assert "message" in Model.model_fields + assert Model.model_fields["message"].default is None + + @pytest.mark.asyncio async def test_resolve_value_plain_value(): """Test resolving a plain, non-callable value.""" From 023eae0069b0eb46ad4827b59a8726c9011a48f6 Mon Sep 17 00:00:00 2001 From: DEVELOPER-DEEVEN <144827577+DEVELOPER-DEEVEN@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:57:18 +0530 Subject: [PATCH 4/5] fix(toolbox-core): annotate Parameter default sentinel as Any --- packages/toolbox-core/src/toolbox_core/protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index a47ff86c1..ae20aeef6 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -109,7 +109,7 @@ def __get_type(self) -> Type: return base_type def to_param(self) -> Parameter: - default_value = Parameter.empty + default_value: Any = Parameter.empty if self.has_default: default_value = self.default elif not self.required: From 6646301841058c32b1d50c24fad0d71c7d02c382 Mon Sep 17 00:00:00 2001 From: DEVELOPER-DEEVEN <144827577+DEVELOPER-DEEVEN@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:36:39 +0530 Subject: [PATCH 5/5] fix(toolbox-core): keep optional signature defaults as None --- packages/toolbox-core/src/toolbox_core/protocol.py | 8 +++++--- packages/toolbox-core/tests/test_protocol.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index ae20aeef6..636dd020c 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -110,10 +110,12 @@ def __get_type(self) -> Type: def to_param(self) -> Parameter: default_value: Any = Parameter.empty - if self.has_default: - default_value = self.default - elif not self.required: + if not self.required: + # Keep optional function signatures stable: optional inputs default to None, + # even when schema includes a backend-side default. default_value = None + elif self.has_default: + default_value = self.default return Parameter( self.name, diff --git a/packages/toolbox-core/tests/test_protocol.py b/packages/toolbox-core/tests/test_protocol.py index fe6320cd4..fa592603c 100644 --- a/packages/toolbox-core/tests/test_protocol.py +++ b/packages/toolbox-core/tests/test_protocol.py @@ -291,8 +291,8 @@ def test_parameter_schema_map_unsupported_value_type_error(): schema._ParameterSchema__get_type() -def test_parameter_schema_with_default(): - """Tests ParameterSchema with a default value provided.""" +def test_parameter_schema_optional_with_default_uses_none_signature_default(): + """Tests optional params keep a None python signature default even with schema defaults.""" schema = ParameterSchema( name="limit", type="integer", @@ -308,7 +308,7 @@ def test_parameter_schema_with_default(): assert isinstance(param, Parameter) assert param.name == "limit" assert param.annotation == expected_type - assert param.default == 10 + assert param.default is None def test_parameter_schema_required_with_default():