From ccce1f1ab8eaaa5180137891a6e16e4d071aac0b Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 6 Feb 2026 15:38:25 +0530 Subject: [PATCH 1/5] fix(adk): Implement generating `FunctionDeclaration` from tool parameters. --- packages/toolbox-adk/src/toolbox_adk/tool.py | 39 +++++++++++ .../tests/integration/test_integration.py | 18 ++++- packages/toolbox-adk/tests/unit/test_tool.py | 66 +++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-adk/src/toolbox_adk/tool.py b/packages/toolbox-adk/src/toolbox_adk/tool.py index 3091640c..7aa14ce7 100644 --- a/packages/toolbox-adk/src/toolbox_adk/tool.py +++ b/packages/toolbox-adk/src/toolbox_adk/tool.py @@ -27,6 +27,7 @@ OAuth2Auth, ) from google.adk.auth.auth_tool import AuthConfig +from google.genai.types import FunctionDeclaration, Type, Schema from google.adk.tools.base_tool import BaseTool from google.adk.tools.tool_context import ToolContext from toolbox_core.tool import ToolboxTool as CoreToolboxTool @@ -79,6 +80,44 @@ def __init__( self._post_hook = post_hook self._auth_config = auth_config + + def _param_type_to_schema_type(self, param_type: str) -> Type: + type_map = { + "string": Type.STRING, + "integer": Type.INTEGER, + "number": Type.NUMBER, + "boolean": Type.BOOLEAN, + "array": Type.ARRAY, + "object": Type.OBJECT, + } + return type_map.get(param_type, Type.STRING) + + @override + def _get_declaration(self) -> Optional[FunctionDeclaration]: + properties = {} + required = [] + + if hasattr(self._core_tool, '_params') and self._core_tool._params: + for param in self._core_tool._params: + properties[param.name] = Schema( + type=self._param_type_to_schema_type(param.type), + description=param.description or "" + ) + if param.required: + required.append(param.name) + + parameters = Schema( + type=Type.OBJECT, + properties=properties, + required=required + ) if properties else None + + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=parameters + ) + @override async def run_async( self, diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 9e0dd467..084d297e 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -60,6 +60,14 @@ async def test_load_toolset_and_run(self): tool = next((t for t in tools if t.name == "get-row-by-id"), None) assert tool is not None assert isinstance(tool, ToolboxTool) + + # Verify the function declaration schema builds correctly end-to-end + declaration = tool._get_declaration() + assert declaration is not None + assert declaration.name == "get-row-by-id" + assert declaration.parameters is not None + assert hasattr(declaration.parameters, 'properties') + assert "id" in declaration.parameters.properties # Run it ctx = MagicMock() @@ -173,6 +181,12 @@ async def test_3lo_flow_simulation(self): tool = tools[0] assert isinstance(tool, ToolboxTool) assert tool.name == "get-n-rows" + + # Verify the function declaration schema builds correctly end-to-end + declaration = tool._get_declaration() + assert declaration is not None + assert declaration.name == "get-n-rows" + assert "num_rows" in declaration.parameters.properties # Create a mock context that behaves like ADK's ReadonlyContext mock_ctx_first = MagicMock() @@ -183,9 +197,7 @@ async def test_3lo_flow_simulation(self): result_first = await tool.run_async({"num_rows": "1"}, mock_ctx_first) # The wrapper should catch the missing creds and request them. - assert ( - result_first is None - ), "Tool should return None to signal auth requirement" + assert isinstance(result_first, dict) and "error" in result_first, "Tool should return error sig for auth requirement" mock_ctx_first.request_credential.assert_called_once() # Inspect the requested config diff --git a/packages/toolbox-adk/tests/unit/test_tool.py b/packages/toolbox-adk/tests/unit/test_tool.py index e0964bcc..56ab53cc 100644 --- a/packages/toolbox-adk/tests/unit/test_tool.py +++ b/packages/toolbox-adk/tests/unit/test_tool.py @@ -267,6 +267,72 @@ async def test_3lo_exception_fallback(self): # Should catch RuntimeError, call request_credential, and return None assert result is None ctx.request_credential.assert_called_once() + + def test_param_type_to_schema_type(self): + core_tool = MagicMock() + core_tool.__name__ = "mock_tool" + core_tool.__doc__ = "mock doc" + tool = ToolboxTool(core_tool) + + from google.genai.types import Type + assert tool._param_type_to_schema_type("string") == Type.STRING + assert tool._param_type_to_schema_type("integer") == Type.INTEGER + assert tool._param_type_to_schema_type("boolean") == Type.BOOLEAN + assert tool._param_type_to_schema_type("number") == Type.NUMBER + assert tool._param_type_to_schema_type("array") == Type.ARRAY + assert tool._param_type_to_schema_type("object") == Type.OBJECT + assert tool._param_type_to_schema_type("unknown") == Type.STRING + + def test_get_declaration(self): + # Create a mock for core tool parameters + class MockParam: + def __init__(self, name, param_type, description, required): + self.name = name + self.type = param_type + self.description = description + self.required = required + + core_tool = MagicMock() + core_tool.__name__ = "mock_tool" + core_tool.__doc__ = "mock doc" + core_tool._params = [ + MockParam("city", "string", "The city name", True), + MockParam("count", "integer", "Number of results", False) + ] + + tool = ToolboxTool(core_tool) + declaration = tool._get_declaration() + + from google.genai.types import Type + assert declaration.name == "mock_tool" + assert declaration.description == "mock doc" + + parameters = declaration.parameters + assert parameters is not None + assert parameters.type == Type.OBJECT + assert "city" in parameters.properties + assert "count" in parameters.properties + + assert parameters.properties["city"].type == Type.STRING + assert parameters.properties["city"].description == "The city name" + + assert parameters.properties["count"].type == Type.INTEGER + assert parameters.properties["count"].description == "Number of results" + + assert parameters.required == ["city"] + + def test_get_declaration_no_params(self): + core_tool = MagicMock() + core_tool.__name__ = "mock_tool" + core_tool.__doc__ = "mock doc" + core_tool._params = [] + + tool = ToolboxTool(core_tool) + declaration = tool._get_declaration() + + assert declaration.name == "mock_tool" + assert declaration.description == "mock doc" + assert getattr(declaration, "parameters", None) is None def test_init_defaults(self): # Test initialization with minimal tool metadata checks From 850d006266827e386119217fecda12a0fe18c38a Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 7 Feb 2026 04:07:44 +0530 Subject: [PATCH 2/5] test(adk): fix test_3lo_flow_simulation by properly mocking credential_service --- .../toolbox-adk/tests/integration/test_integration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 084d297e..c7088e08 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -16,7 +16,7 @@ import os from typing import Any, Optional from inspect import signature, Parameter -from unittest.mock import MagicMock +from unittest.mock import MagicMock, AsyncMock import pytest from pydantic import ValidationError @@ -192,12 +192,16 @@ async def test_3lo_flow_simulation(self): mock_ctx_first = MagicMock() # Simulate "No Auth Response Found" mock_ctx_first.get_auth_response.return_value = None + mock_cred_service_first = AsyncMock() + mock_cred_service_first.load_credential.return_value = None + mock_ctx_first._invocation_context = MagicMock() + mock_ctx_first._invocation_context.credential_service = mock_cred_service_first print("Running tool first time (expecting auth request)...") result_first = await tool.run_async({"num_rows": "1"}, mock_ctx_first) # The wrapper should catch the missing creds and request them. - assert isinstance(result_first, dict) and "error" in result_first, "Tool should return error sig for auth requirement" + assert result_first is None, "Tool should return None sig for auth requirement" mock_ctx_first.request_credential.assert_called_once() # Inspect the requested config From b503e89ce6baf37e119dc8fc8ceb8ed6f588e935 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 9 Feb 2026 12:52:31 +0530 Subject: [PATCH 3/5] chore: improve comments --- packages/toolbox-adk/src/toolbox_adk/tool.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-adk/src/toolbox_adk/tool.py b/packages/toolbox-adk/src/toolbox_adk/tool.py index 7aa14ce7..4fba5f2f 100644 --- a/packages/toolbox-adk/src/toolbox_adk/tool.py +++ b/packages/toolbox-adk/src/toolbox_adk/tool.py @@ -94,9 +94,13 @@ def _param_type_to_schema_type(self, param_type: str) -> Type: @override def _get_declaration(self) -> Optional[FunctionDeclaration]: + """Gets the function declaration for the tool.""" properties = {} required = [] + # We do not use `google.genai.types.FunctionDeclaration.from_callable` here + # because it explicitly drops argument descriptions from the schema properties, + # lumping them all into the root description instead. if hasattr(self._core_tool, '_params') and self._core_tool._params: for param in self._core_tool._params: properties[param.name] = Schema( @@ -105,13 +109,13 @@ def _get_declaration(self) -> Optional[FunctionDeclaration]: ) if param.required: required.append(param.name) - + parameters = Schema( type=Type.OBJECT, properties=properties, required=required ) if properties else None - + return FunctionDeclaration( name=self.name, description=self.description, @@ -124,11 +128,9 @@ async def run_async( args: Dict[str, Any], tool_context: ToolContext, ) -> Any: - # 1. Pre-hook if self._pre_hook: await self._pre_hook(tool_context, args) - # 2. ADK Auth Integration (3LO) # Check if USER_IDENTITY is configured reset_token = None From ea6186ca0c0bf43b5c146baa4bc8f1a7eaebafe1 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 9 Feb 2026 14:10:41 +0530 Subject: [PATCH 4/5] chore: delint --- packages/toolbox-adk/src/toolbox_adk/tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-adk/src/toolbox_adk/tool.py b/packages/toolbox-adk/src/toolbox_adk/tool.py index 4fba5f2f..1b0476f0 100644 --- a/packages/toolbox-adk/src/toolbox_adk/tool.py +++ b/packages/toolbox-adk/src/toolbox_adk/tool.py @@ -98,9 +98,9 @@ def _get_declaration(self) -> Optional[FunctionDeclaration]: properties = {} required = [] - # We do not use `google.genai.types.FunctionDeclaration.from_callable` here - # because it explicitly drops argument descriptions from the schema properties, - # lumping them all into the root description instead. + # We do not use `google.genai.types.FunctionDeclaration.from_callable` + # here because it explicitly drops argument descriptions from the schema + # properties, lumping them all into the root description instead. if hasattr(self._core_tool, '_params') and self._core_tool._params: for param in self._core_tool._params: properties[param.name] = Schema( From 352e8e68c0e70eec65fb907353a6f1a77e3c1264 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 9 Feb 2026 17:44:03 +0530 Subject: [PATCH 5/5] chore: move `google.genai.types` import to top of file. --- packages/toolbox-adk/tests/unit/test_tool.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/toolbox-adk/tests/unit/test_tool.py b/packages/toolbox-adk/tests/unit/test_tool.py index 56ab53cc..6c7c7a14 100644 --- a/packages/toolbox-adk/tests/unit/test_tool.py +++ b/packages/toolbox-adk/tests/unit/test_tool.py @@ -18,6 +18,7 @@ from toolbox_adk.credentials import CredentialConfig, CredentialType from toolbox_adk.tool import ToolboxTool +from google.genai.types import Type class TestToolboxTool: @@ -274,7 +275,6 @@ def test_param_type_to_schema_type(self): core_tool.__doc__ = "mock doc" tool = ToolboxTool(core_tool) - from google.genai.types import Type assert tool._param_type_to_schema_type("string") == Type.STRING assert tool._param_type_to_schema_type("integer") == Type.INTEGER assert tool._param_type_to_schema_type("boolean") == Type.BOOLEAN @@ -303,7 +303,6 @@ def __init__(self, name, param_type, description, required): tool = ToolboxTool(core_tool) declaration = tool._get_declaration() - from google.genai.types import Type assert declaration.name == "mock_tool" assert declaration.description == "mock doc"