From 82dfa302a0ec928076bbef4fb6c255ff2b587ba1 Mon Sep 17 00:00:00 2001 From: Deeven Seru Date: Tue, 10 Feb 2026 10:27:59 +0000 Subject: [PATCH 1/4] 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 e522d792..f76cc1b8 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 8dd60e3f..1e86a02b 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 ebc18a5766d39938af551eeda06f3daf86b3831c Mon Sep 17 00:00:00 2001 From: Deeven Seru Date: Tue, 10 Feb 2026 11:12:23 +0000 Subject: [PATCH 2/4] 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 0c72c39b..93a693d8 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 from warnings import warn @@ -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 08a87a45..98b8217c 100644 --- a/packages/toolbox-core/src/toolbox_core/utils.py +++ b/packages/toolbox-core/src/toolbox_core/utils.py @@ -111,10 +111,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 dbeb66d79989b6a6b9d1f39481c557e65da7130a 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/4] 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 f76cc1b8..a47ff86c 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 93a693d8..128e57cd 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 98b8217c..d68f585a 100644 --- a/packages/toolbox-core/src/toolbox_core/utils.py +++ b/packages/toolbox-core/src/toolbox_core/utils.py @@ -115,7 +115,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 1e86a02b..fe6320cd 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 b3ddd7c3..5c9dd29d 100644 --- a/packages/toolbox-core/tests/test_utils.py +++ b/packages/toolbox-core/tests/test_utils.py @@ -35,6 +35,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 @@ -422,6 +424,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 a8d7181853eb7c3c703553564a0f6ac12e7f8a3b 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/4] 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 a47ff86c..ae20aeef 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: