From b5bd7afe9958dfaafd9953e2ea3117e4cdfc4ae2 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:00:27 +0530 Subject: [PATCH 01/20] feat(toolbox-core): Add support for optional parameters --- .../toolbox-core/src/toolbox_core/protocol.py | 24 ++++++++++++++----- .../toolbox-core/src/toolbox_core/tool.py | 12 +++++++--- .../toolbox-core/src/toolbox_core/utils.py | 11 ++++++++- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index e2071ba2..a825d511 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -25,31 +25,43 @@ class ParameterSchema(BaseModel): name: str type: str + required: bool = True description: str authSources: Optional[list[str]] = None items: Optional["ParameterSchema"] = None def __get_type(self) -> Type: + base_type: Type if self.type == "string": - return str + base_type = str elif self.type == "integer": - return int + base_type = int elif self.type == "float": - return float + base_type = float elif self.type == "boolean": - return bool + base_type = bool elif self.type == "array": if self.items is None: raise Exception("Unexpected value: type is 'list' but items is None") - return list[self.items.__get_type()] # type: ignore + base_type = list[self.items.__get_type()] + else: + raise ValueError(f"Unsupported schema type: {self.type}") - raise ValueError(f"Unsupported schema type: {self.type}") + if not self.required: + return Optional[base_type] + + return base_type def to_param(self) -> Parameter: + default = Parameter.empty + if not self.required: + default = None + return Parameter( self.name, Parameter.POSITIONAL_OR_KEYWORD, annotation=self.__get_type(), + default=default, ) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 48a31dab..a11b0688 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -13,7 +13,7 @@ # limitations under the License. import copy -from inspect import Signature +from inspect import Signature, Parameter from types import MappingProxyType from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union from warnings import warn @@ -89,7 +89,11 @@ def __init__( self.__params = params self.__pydantic_model = params_to_pydantic_model(name, self.__params) - inspect_type_params = [param.to_param() for param in self.__params] + # Sort parameters to ensure required ones (required=True) come before + # optional ones (required=False). This prevents the "non-default argument + # follows default argument" error when creating the signature. + sorted_params = sorted(self.__params, key=lambda p: p.required, reverse=True) + inspect_type_params = [param.to_param() for param in sorted_params] # the following properties are set to help anyone that might inspect it determine usage self.__name__ = name @@ -268,7 +272,9 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str: # validate inputs to this call using the signature all_args = self.__signature__.bind(*args, **kwargs) - all_args.apply_defaults() # Include default values if not provided + + # The payload will only contain arguments explicitly provided by the user. + # Optional arguments not provided by the user will not be in the payload. payload = all_args.arguments # Perform argument type validations using pydantic diff --git a/packages/toolbox-core/src/toolbox_core/utils.py b/packages/toolbox-core/src/toolbox_core/utils.py index 615a23ec..28c046b1 100644 --- a/packages/toolbox-core/src/toolbox_core/utils.py +++ b/packages/toolbox-core/src/toolbox_core/utils.py @@ -111,11 +111,20 @@ def params_to_pydantic_model( """Converts the given parameters to a Pydantic BaseModel class.""" field_definitions = {} for field in params: + + # Determine the default value based on the 'required' flag. + # '...' (Ellipsis) signifies a required field in Pydantic. + # 'None' makes the field optional with a default value of None. + default_value = ... if field.required else None + field_definitions[field.name] = cast( Any, ( field.to_param().annotation, - Field(description=field.description), + Field( + description=field.description, + default=default_value, + ), ), ) return create_model(tool_name, **field_definitions) From 335c60730d1481311080b627e5b1961d9b7a7ccc Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:14:12 +0530 Subject: [PATCH 02/20] chore: Delint --- 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 a825d511..cc61b5fd 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -56,7 +56,7 @@ def to_param(self) -> Parameter: default = Parameter.empty if not self.required: default = None - + return Parameter( self.name, Parameter.POSITIONAL_OR_KEYWORD, From 10c9bff8af814ca87d8ba296881b09bf07c4d006 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:17:09 +0530 Subject: [PATCH 03/20] chore: Remove unnecessary import --- packages/toolbox-core/src/toolbox_core/tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index a11b0688..b5d5bba1 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -13,7 +13,7 @@ # limitations under the License. import copy -from inspect import Signature, Parameter +from inspect import Signature from types import MappingProxyType from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union from warnings import warn From fefc78b913cc5be0e8e6f58187c89fcc7bdde2b1 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:30:48 +0530 Subject: [PATCH 04/20] chore: Delint --- packages/toolbox-core/src/toolbox_core/protocol.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index cc61b5fd..d5093e1e 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -43,25 +43,21 @@ def __get_type(self) -> Type: elif self.type == "array": if self.items is None: raise Exception("Unexpected value: type is 'list' but items is None") - base_type = list[self.items.__get_type()] + base_type = list[self.items.__get_type()] # type: ignore else: raise ValueError(f"Unsupported schema type: {self.type}") if not self.required: - return Optional[base_type] + return Optional[base_type] # type: ignore return base_type def to_param(self) -> Parameter: - default = Parameter.empty - if not self.required: - default = None - return Parameter( self.name, Parameter.POSITIONAL_OR_KEYWORD, annotation=self.__get_type(), - default=default, + default= Parameter.empty if self.required else None, ) From d7318c6ae6ec4dbc7a048a2b41bef8ad21a4c26a Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:34:28 +0530 Subject: [PATCH 05/20] chore: Delint --- 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 d5093e1e..6606ef93 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -57,7 +57,7 @@ def to_param(self) -> Parameter: self.name, Parameter.POSITIONAL_OR_KEYWORD, annotation=self.__get_type(), - default= Parameter.empty if self.required else None, + default=Parameter.empty if self.required else None, ) From dcf242cf1c9151442b13e64a425558e7f44767a9 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:38:21 +0530 Subject: [PATCH 06/20] fix: Fix unit tests --- packages/toolbox-core/tests/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolbox-core/tests/test_utils.py b/packages/toolbox-core/tests/test_utils.py index c07f44cb..b3ddd7c3 100644 --- a/packages/toolbox-core/tests/test_utils.py +++ b/packages/toolbox-core/tests/test_utils.py @@ -34,6 +34,7 @@ def create_param_mock(name: str, description: str, annotation: Type) -> Mock: param_mock = Mock(spec=ParameterSchema) param_mock.name = name param_mock.description = description + param_mock.required = True mock_param_info = Mock() mock_param_info.annotation = annotation From dcf3111541c582750840cefd07360a1cc492d910 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:43:50 +0530 Subject: [PATCH 07/20] chore: Add unit tests for optional parameters --- packages/toolbox-core/tests/test_protocol.py | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/toolbox-core/tests/test_protocol.py b/packages/toolbox-core/tests/test_protocol.py index a70fa3fe..66f6e66c 100644 --- a/packages/toolbox-core/tests/test_protocol.py +++ b/packages/toolbox-core/tests/test_protocol.py @@ -14,6 +14,7 @@ from inspect import Parameter +from typing import Optional import pytest @@ -106,3 +107,65 @@ def test_parameter_schema_unsupported_type_error(): with pytest.raises(ValueError, match=expected_error_msg): schema.to_param() + +def test_parameter_schema_string_optional(): + """Tests an optional ParameterSchema with type 'string'.""" + schema = ParameterSchema( + name="nickname", + type="string", + description="An optional nickname", + required=False, + ) + expected_type = Optional[str] + + # Test __get_type() + assert schema._ParameterSchema__get_type() == expected_type + + # Test to_param() + param = schema.to_param() + assert isinstance(param, Parameter) + assert param.name == "nickname" + assert param.annotation == expected_type + assert param.kind == Parameter.POSITIONAL_OR_KEYWORD + assert param.default is None + + +def test_parameter_schema_required_by_default(): + """Tests that a parameter is required by default.""" + # 'required' is not specified, so it should default to True. + schema = ParameterSchema(name="id", type="integer", description="A required ID") + expected_type = int + + # Test __get_type() + assert schema._ParameterSchema__get_type() == expected_type + + # Test to_param() + param = schema.to_param() + assert isinstance(param, Parameter) + assert param.name == "id" + assert param.annotation == expected_type + assert param.default == Parameter.empty + + +def test_parameter_schema_array_optional(): + """Tests an optional ParameterSchema with type 'array'.""" + item_schema = ParameterSchema(name="", type="integer", description="") + schema = ParameterSchema( + name="optional_scores", + type="array", + description="An optional list of scores", + items=item_schema, + required=False, + ) + expected_type = Optional[list[int]] + + # Test __get_type() + assert schema._ParameterSchema__get_type() == expected_type + + # Test to_param() + param = schema.to_param() + assert isinstance(param, Parameter) + assert param.name == "optional_scores" + assert param.annotation == expected_type + assert param.kind == Parameter.POSITIONAL_OR_KEYWORD + assert param.default is None \ No newline at end of file From 49bac747849f16a1777aaf13ba28f48009676b04 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 13:47:34 +0530 Subject: [PATCH 08/20] chore: Delint --- packages/toolbox-core/tests/test_protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_protocol.py b/packages/toolbox-core/tests/test_protocol.py index 66f6e66c..b7650792 100644 --- a/packages/toolbox-core/tests/test_protocol.py +++ b/packages/toolbox-core/tests/test_protocol.py @@ -108,6 +108,7 @@ def test_parameter_schema_unsupported_type_error(): with pytest.raises(ValueError, match=expected_error_msg): schema.to_param() + def test_parameter_schema_string_optional(): """Tests an optional ParameterSchema with type 'string'.""" schema = ParameterSchema( @@ -168,4 +169,4 @@ def test_parameter_schema_array_optional(): assert param.name == "optional_scores" assert param.annotation == expected_type assert param.kind == Parameter.POSITIONAL_OR_KEYWORD - assert param.default is None \ No newline at end of file + assert param.default is None From 54957bbffcb9b699f1d8458abaddcb99a6e14e77 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 16:17:26 +0530 Subject: [PATCH 09/20] chore: Add E2E tests for optional params --- packages/toolbox-core/tests/test_e2e.py | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 8920bc3b..ab20bc0d 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -11,9 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import pytest import pytest_asyncio from pydantic import ValidationError +from inspect import signature, Parameter +from typing import Optional from toolbox_core.client import ToolboxClient from toolbox_core.tool import ToolboxTool @@ -217,3 +220,56 @@ async def test_run_tool_param_auth_no_field( match="no field named row_data in claims", ): await tool() + +@pytest.mark.asyncio +@pytest.mark.usefixtures("optional_param_server") +class TestOptionalParams: + """ + End-to-end tests for tools with optional parameters. + """ + + async def test_tool_signature_is_correct(self, optional_toolbox: ToolboxClient): + """Verify the client correctly constructs the signature for a tool with optional params.""" + tool = await optional_toolbox.load_tool("search-rows") + sig = signature(tool) + + assert "query" in sig.parameters + assert "limit" in sig.parameters + + # The required parameter should have no default + assert sig.parameters["query"].default is Parameter.empty + assert sig.parameters["query"].annotation is str + + # The optional parameter should have a default of None + assert sig.parameters["limit"].default is None + assert sig.parameters["limit"].annotation is Optional[int] + + async def test_run_tool_with_optional_param_omitted( + self, optional_toolbox: ToolboxClient + ): + """Invoke a tool providing only the required parameter.""" + tool = await optional_toolbox.load_tool("search-rows") + + response = await tool(query="test query") + assert isinstance(response, str) + assert 'query="test query"' in response + assert "limit" not in response + + async def test_run_tool_with_optional_param_provided( + self, optional_toolbox: ToolboxClient + ): + """Invoke a tool providing both required and optional parameters.""" + tool = await optional_toolbox.load_tool("search-rows") + + response = await tool(query="test query", limit=10) + assert isinstance(response, str) + assert 'query="test query"' in response + assert "limit=10" in response + + async def test_run_tool_with_missing_required_param( + self, optional_toolbox: ToolboxClient + ): + """Invoke a tool without its required parameter.""" + tool = await optional_toolbox.load_tool("search-rows") + with pytest.raises(TypeError, match="missing a required argument: 'query'"): + await tool(limit=5) From 1a2658ddb83688b38e68f7a8613936edf2628c6e Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 16:18:46 +0530 Subject: [PATCH 10/20] chore: Delint --- packages/toolbox-core/tests/test_e2e.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index ab20bc0d..ed53c61c 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from inspect import Parameter, signature +from typing import Optional + import pytest import pytest_asyncio from pydantic import ValidationError -from inspect import signature, Parameter -from typing import Optional from toolbox_core.client import ToolboxClient from toolbox_core.tool import ToolboxTool @@ -221,6 +222,7 @@ async def test_run_tool_param_auth_no_field( ): await tool() + @pytest.mark.asyncio @pytest.mark.usefixtures("optional_param_server") class TestOptionalParams: From e916457612be0b559ec6d0d2d3564f4ccee3f9b2 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 16:22:24 +0530 Subject: [PATCH 11/20] fix: Fix e2e tests --- packages/toolbox-core/tests/test_e2e.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index ed53c61c..03dd57a7 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -224,15 +224,15 @@ async def test_run_tool_param_auth_no_field( @pytest.mark.asyncio -@pytest.mark.usefixtures("optional_param_server") +@pytest.mark.usefixtures("toolbox_server") class TestOptionalParams: """ End-to-end tests for tools with optional parameters. """ - async def test_tool_signature_is_correct(self, optional_toolbox: ToolboxClient): + async def test_tool_signature_is_correct(self, toolbox: ToolboxClient): """Verify the client correctly constructs the signature for a tool with optional params.""" - tool = await optional_toolbox.load_tool("search-rows") + tool = await toolbox.load_tool("search-rows") sig = signature(tool) assert "query" in sig.parameters @@ -247,10 +247,10 @@ async def test_tool_signature_is_correct(self, optional_toolbox: ToolboxClient): assert sig.parameters["limit"].annotation is Optional[int] async def test_run_tool_with_optional_param_omitted( - self, optional_toolbox: ToolboxClient + self, toolbox: ToolboxClient ): """Invoke a tool providing only the required parameter.""" - tool = await optional_toolbox.load_tool("search-rows") + tool = await toolbox.load_tool("search-rows") response = await tool(query="test query") assert isinstance(response, str) @@ -258,10 +258,10 @@ async def test_run_tool_with_optional_param_omitted( assert "limit" not in response async def test_run_tool_with_optional_param_provided( - self, optional_toolbox: ToolboxClient + self, toolbox: ToolboxClient ): """Invoke a tool providing both required and optional parameters.""" - tool = await optional_toolbox.load_tool("search-rows") + tool = await toolbox.load_tool("search-rows") response = await tool(query="test query", limit=10) assert isinstance(response, str) @@ -269,9 +269,9 @@ async def test_run_tool_with_optional_param_provided( assert "limit=10" in response async def test_run_tool_with_missing_required_param( - self, optional_toolbox: ToolboxClient + self, toolbox: ToolboxClient ): """Invoke a tool without its required parameter.""" - tool = await optional_toolbox.load_tool("search-rows") + tool = await toolbox.load_tool("search-rows") with pytest.raises(TypeError, match="missing a required argument: 'query'"): await tool(limit=5) From 809ddf9e35d30200b4936ec8d1fd3ecbd341f2e0 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 16:23:33 +0530 Subject: [PATCH 12/20] chore: Delint --- packages/toolbox-core/tests/test_e2e.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 03dd57a7..02525b52 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -246,9 +246,7 @@ async def test_tool_signature_is_correct(self, toolbox: ToolboxClient): assert sig.parameters["limit"].default is None assert sig.parameters["limit"].annotation is Optional[int] - async def test_run_tool_with_optional_param_omitted( - self, toolbox: ToolboxClient - ): + async def test_run_tool_with_optional_param_omitted(self, toolbox: ToolboxClient): """Invoke a tool providing only the required parameter.""" tool = await toolbox.load_tool("search-rows") @@ -257,9 +255,7 @@ async def test_run_tool_with_optional_param_omitted( assert 'query="test query"' in response assert "limit" not in response - async def test_run_tool_with_optional_param_provided( - self, toolbox: ToolboxClient - ): + async def test_run_tool_with_optional_param_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") @@ -268,9 +264,7 @@ async def test_run_tool_with_optional_param_provided( assert 'query="test query"' in response assert "limit=10" in response - async def test_run_tool_with_missing_required_param( - self, toolbox: ToolboxClient - ): + async def test_run_tool_with_missing_required_param(self, toolbox: ToolboxClient): """Invoke a tool without its required parameter.""" tool = await toolbox.load_tool("search-rows") with pytest.raises(TypeError, match="missing a required argument: 'query'"): From 4decbad825ab9b8541b4001d975bafd35f351c57 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 16:50:40 +0530 Subject: [PATCH 13/20] chore: Update e2e tests --- packages/toolbox-core/tests/test_e2e.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 02525b52..07269476 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -250,16 +250,25 @@ async def test_run_tool_with_optional_param_omitted(self, toolbox: ToolboxClient """Invoke a tool providing only the required parameter.""" tool = await toolbox.load_tool("search-rows") - response = await tool(query="test query") + response = await tool(email="107716898620-compute@developer.gserviceaccount.com") assert isinstance(response, str) assert 'query="test query"' in response assert "limit" not in response - async def test_run_tool_with_optional_param_provided(self, toolbox: ToolboxClient): + async def test_run_tool_with_optional_data_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") - response = await tool(query="test query", limit=10) + response = await tool(email="107716898620-compute@developer.gserviceaccount.com", data="row2") + assert isinstance(response, str) + assert 'query="test query"' in response + assert "limit=10" in response + + async def test_run_tool_with_optional_id_provided(self, toolbox: ToolboxClient): + """Invoke a tool providing both required and optional parameters.""" + tool = await toolbox.load_tool("search-rows") + + response = await tool(email="107716898620-compute@developer.gserviceaccount.com", id=1) assert isinstance(response, str) assert 'query="test query"' in response assert "limit=10" in response @@ -267,5 +276,5 @@ async def test_run_tool_with_optional_param_provided(self, toolbox: ToolboxClien async def test_run_tool_with_missing_required_param(self, toolbox: ToolboxClient): """Invoke a tool without its required parameter.""" tool = await toolbox.load_tool("search-rows") - with pytest.raises(TypeError, match="missing a required argument: 'query'"): - await tool(limit=5) + with pytest.raises(TypeError, match="missing a required argument: 'email'"): + await tool(id=2, data="row2") From 29da6033cd245a033cf099d144252620b7506344 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 17:04:56 +0530 Subject: [PATCH 14/20] chore: Improve e2e tests --- packages/toolbox-core/tests/test_e2e.py | 53 ++++++++++++++++++------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 07269476..642e5e91 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -239,42 +239,67 @@ async def test_tool_signature_is_correct(self, toolbox: ToolboxClient): assert "limit" in sig.parameters # The required parameter should have no default - assert sig.parameters["query"].default is Parameter.empty - assert sig.parameters["query"].annotation is str + assert sig.parameters["email"].default is Parameter.empty + assert sig.parameters["email"].annotation is str # The optional parameter should have a default of None - assert sig.parameters["limit"].default is None - assert sig.parameters["limit"].annotation is Optional[int] + assert sig.parameters["data"].default is "row2" + assert sig.parameters["limit"].annotation is Optional[str] + + # The optional parameter should have a default of None + assert sig.parameters["id"].default is None + assert sig.parameters["id"].annotation is Optional[int] async def test_run_tool_with_optional_param_omitted(self, toolbox: ToolboxClient): """Invoke a tool providing only the required parameter.""" tool = await toolbox.load_tool("search-rows") - response = await tool(email="107716898620-compute@developer.gserviceaccount.com") + response = await tool( + email="twishabansal@google.com" + ) assert isinstance(response, str) - assert 'query="test query"' in response - assert "limit" not in response + assert 'email="twishabansal@google.com"' in response + assert "row1" not in response + assert "row2" in response + assert "row3" not in response + assert "row4" not in response + assert "row5" not in response + assert "row6" not in response async def test_run_tool_with_optional_data_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") - response = await tool(email="107716898620-compute@developer.gserviceaccount.com", data="row2") + response = await tool( + email="twishabansal@google.com", data="row3" + ) assert isinstance(response, str) - assert 'query="test query"' in response - assert "limit=10" in response + assert 'email="twishabansal@google.com"' in response + assert "row1" not in response + assert "row2" not in response + assert "row3" in response + assert "row4" not in response + assert "row5" not in response + assert "row6" not in response async def test_run_tool_with_optional_id_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") - response = await tool(email="107716898620-compute@developer.gserviceaccount.com", id=1) + response = await tool( + email="twishabansal@google.com", id=1 + ) assert isinstance(response, str) - assert 'query="test query"' in response - assert "limit=10" in response + assert 'email="twishabansal@google.com"' in response + assert "row1" in response + assert "row2" not in response + assert "row3" not in response + assert "row4" not in response + assert "row5" not in response + assert "row6" not in response async def test_run_tool_with_missing_required_param(self, toolbox: ToolboxClient): """Invoke a tool without its required parameter.""" tool = await toolbox.load_tool("search-rows") with pytest.raises(TypeError, match="missing a required argument: 'email'"): - await tool(id=2, data="row2") + await tool(id=5, data="row5") From 1dc2ca253cde13a2415a96a2e0fb97c3cf7439ed Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 17:05:34 +0530 Subject: [PATCH 15/20] chore: Delint --- packages/toolbox-core/tests/test_e2e.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 642e5e91..6cb89edd 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -254,9 +254,7 @@ async def test_run_tool_with_optional_param_omitted(self, toolbox: ToolboxClient """Invoke a tool providing only the required parameter.""" tool = await toolbox.load_tool("search-rows") - response = await tool( - email="twishabansal@google.com" - ) + response = await tool(email="twishabansal@google.com") assert isinstance(response, str) assert 'email="twishabansal@google.com"' in response assert "row1" not in response @@ -270,9 +268,7 @@ async def test_run_tool_with_optional_data_provided(self, toolbox: ToolboxClient """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") - response = await tool( - email="twishabansal@google.com", data="row3" - ) + response = await tool(email="twishabansal@google.com", data="row3") assert isinstance(response, str) assert 'email="twishabansal@google.com"' in response assert "row1" not in response @@ -286,9 +282,7 @@ async def test_run_tool_with_optional_id_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") - response = await tool( - email="twishabansal@google.com", id=1 - ) + response = await tool(email="twishabansal@google.com", id=1) assert isinstance(response, str) assert 'email="twishabansal@google.com"' in response assert "row1" in response From 917c1d3d626014924998ca644aff7cd217651090 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 18:55:50 +0530 Subject: [PATCH 16/20] chore: Improve e2e tests for optional params around null values --- packages/toolbox-core/tests/test_e2e.py | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 6cb89edd..650968fb 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -250,7 +250,7 @@ async def test_tool_signature_is_correct(self, toolbox: ToolboxClient): assert sig.parameters["id"].default is None assert sig.parameters["id"].annotation is Optional[int] - async def test_run_tool_with_optional_param_omitted(self, toolbox: ToolboxClient): + async def test_run_tool_with_optional_params_omitted(self, toolbox: ToolboxClient): """Invoke a tool providing only the required parameter.""" tool = await toolbox.load_tool("search-rows") @@ -278,6 +278,21 @@ async def test_run_tool_with_optional_data_provided(self, toolbox: ToolboxClient assert "row5" not in response assert "row6" not in response + async def test_run_tool_with_optional_data_null(self, toolbox: ToolboxClient): + """Invoke a tool providing both required and optional parameters.""" + tool = await toolbox.load_tool("search-rows") + + response = await tool(email="twishabansal@google.com", data=None) + assert isinstance(response, str) + assert 'email="twishabansal@google.com"' in response + assert "row1" not in response + assert "row2" in response + assert "row3" not in response + assert "row4" not in response + assert "row5" not in response + assert "row6" not in response + + async def test_run_tool_with_optional_id_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") @@ -292,8 +307,28 @@ async def test_run_tool_with_optional_id_provided(self, toolbox: ToolboxClient): assert "row5" not in response assert "row6" not in response + async def test_run_tool_with_optional_id_null(self, toolbox: ToolboxClient): + """Invoke a tool providing both required and optional parameters.""" + tool = await toolbox.load_tool("search-rows") + + response = await tool(email="twishabansal@google.com", id=None) + assert isinstance(response, str) + assert 'email="twishabansal@google.com"' in response + assert "row1" not in response + assert "row2" in response + assert "row3" not in response + assert "row4" not in response + assert "row5" not in response + assert "row6" not in response + async def test_run_tool_with_missing_required_param(self, toolbox: ToolboxClient): """Invoke a tool without its required parameter.""" tool = await toolbox.load_tool("search-rows") with pytest.raises(TypeError, match="missing a required argument: 'email'"): await tool(id=5, data="row5") + + async def test_run_tool_with_required_param_null(self, toolbox: ToolboxClient): + """Invoke a tool without its required parameter.""" + tool = await toolbox.load_tool("search-rows") + with pytest.raises(TypeError, match="missing a required argument: 'email'"): + await tool(email=None, id=5, data="row5") From de27831e03731793ff59c2891215d407e42ed818 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 19 Jun 2025 19:01:31 +0530 Subject: [PATCH 17/20] chore: Delint --- packages/toolbox-core/tests/test_e2e.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 650968fb..de25262b 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -292,7 +292,6 @@ async def test_run_tool_with_optional_data_null(self, toolbox: ToolboxClient): assert "row5" not in response assert "row6" not in response - async def test_run_tool_with_optional_id_provided(self, toolbox: ToolboxClient): """Invoke a tool providing both required and optional parameters.""" tool = await toolbox.load_tool("search-rows") From 2a47fd493e49f07c8100c317709c11f3419cb922 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 20 Jun 2025 10:38:17 +0530 Subject: [PATCH 18/20] chore: Make separation of required/optional params more efficient --- packages/toolbox-core/src/toolbox_core/tool.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index b5d5bba1..0321468f 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -89,11 +89,13 @@ def __init__( self.__params = params self.__pydantic_model = params_to_pydantic_model(name, self.__params) - # Sort parameters to ensure required ones (required=True) come before - # optional ones (required=False). This prevents the "non-default argument - # follows default argument" error when creating the signature. - sorted_params = sorted(self.__params, key=lambda p: p.required, reverse=True) - inspect_type_params = [param.to_param() for param in sorted_params] + # Separate parameters into required (no default) and optional (with + # 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 = required_params + optional_params + inspect_type_params = [param.to_param() for param in ordered_params] # the following properties are set to help anyone that might inspect it determine usage self.__name__ = name @@ -468,4 +470,4 @@ def bind_param( the tool's definition. """ - return self.bind_params({param_name: param_value}) + return self.bind_params({param_name: param_value}) \ No newline at end of file From 03d573d16c409068906270c5d6af6f3eb19695be Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 20 Jun 2025 10:54:02 +0530 Subject: [PATCH 19/20] chore: Delint --- packages/toolbox-core/src/toolbox_core/tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 0321468f..4ecd9cb0 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -470,4 +470,4 @@ def bind_param( the tool's definition. """ - return self.bind_params({param_name: param_value}) \ No newline at end of file + return self.bind_params({param_name: param_value}) From d2d2027ab95177b62340d9c7d9eb2b4f3c755be3 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 23 Jun 2025 21:45:00 +0530 Subject: [PATCH 20/20] chore: optimize chaining required and optional params --- packages/toolbox-core/src/toolbox_core/tool.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 4ecd9cb0..8d413970 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -13,6 +13,7 @@ # limitations under the License. import copy +import itertools from inspect import Signature from types import MappingProxyType from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union @@ -92,9 +93,9 @@ def __init__( # Separate parameters into required (no default) and optional (with # 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 = required_params + optional_params + 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] # the following properties are set to help anyone that might inspect it determine usage