Skip to content

Commit dc99a58

Browse files
GWealecopybara-github
authored andcommitted
refactor(types): type the tool and toolset base classes for strict mypy
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 974055066
1 parent 391fdc6 commit dc99a58

7 files changed

Lines changed: 93 additions & 21 deletions

File tree

src/google/adk/tools/agent_tool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ async def run_async(
404404
tool_context: ToolContext,
405405
) -> Any:
406406
input_schema = _get_input_schema(self.agent)
407+
node_input: object
407408
if input_schema:
408409
try:
409410
node_input = input_schema.model_validate(args)

src/google/adk/tools/authenticated_function_tool.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(
4545
self,
4646
*,
4747
func: Callable[..., Any],
48-
auth_config: AuthConfig = None,
48+
auth_config: Optional[AuthConfig] = None,
4949
response_for_auth_required: Optional[Union[dict[str, Any], str]] = None,
5050
):
5151
"""Initializes the AuthenticatedFunctionTool.
@@ -65,6 +65,7 @@ def __init__(
6565
super().__init__(func=func)
6666
self._ignore_params.append("credential")
6767

68+
self._credentials_manager: Optional[CredentialManager]
6869
if auth_config and auth_config.auth_scheme:
6970
self._credentials_manager = CredentialManager(auth_config=auth_config)
7071
else:
@@ -98,7 +99,7 @@ async def _run_async_impl(
9899
*,
99100
args: dict[str, Any],
100101
tool_context: ToolContext,
101-
credential: AuthCredential,
102+
credential: Optional[AuthCredential],
102103
) -> Any:
103104
args_to_call = args.copy()
104105
signature = inspect.signature(self.func)

src/google/adk/tools/base_authenticated_tool.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
from abc import abstractmethod
1818
import logging
1919
from typing import Any
20-
from typing import Optional
21-
from typing import Union
20+
from typing import cast
2221

2322
from typing_extensions import override
2423

@@ -43,11 +42,11 @@ class BaseAuthenticatedTool(BaseTool):
4342
def __init__(
4443
self,
4544
*,
46-
name,
47-
description,
48-
auth_config: AuthConfig = None,
49-
response_for_auth_required: Optional[Union[dict[str, Any], str]] = None,
50-
):
45+
name: str,
46+
description: str,
47+
auth_config: AuthConfig | None = None,
48+
response_for_auth_required: dict[str, Any] | str | None = None,
49+
) -> None:
5150
"""
5251
Args:
5352
name: The name of the tool.
@@ -67,6 +66,7 @@ def __init__(
6766
description=description,
6867
)
6968
self._auth_config = auth_config
69+
self._credentials_manager: CredentialManager | None
7070

7171
if auth_config and auth_config.auth_scheme:
7272
self._credentials_manager = CredentialManager(auth_config=auth_config)
@@ -94,7 +94,10 @@ async def run_async(
9494
return await self._run_async_impl(
9595
args=args,
9696
tool_context=tool_context,
97-
credential=credential,
97+
# A tool with no credentials manager runs unauthenticated, so this is
98+
# None on that path. Widening the abstract signature instead would
99+
# invalidate every subclass that declares the narrower type.
100+
credential=cast(AuthCredential, credential),
98101
)
99102

100103
@abstractmethod

src/google/adk/tools/base_tool.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from __future__ import annotations
1616

1717
from abc import ABC
18+
from collections.abc import Callable as CallableABC
1819
import inspect
1920
import logging
2021
from typing import Any
@@ -48,6 +49,11 @@
4849
SelfTool = TypeVar("SelfTool", bound="BaseTool")
4950

5051

52+
def _is_callable_annotation(annotation: object) -> bool:
53+
"""Returns whether a resolved annotation describes a callable."""
54+
return annotation is Callable or get_origin(annotation) is CallableABC
55+
56+
5157
class BaseTool(ABC):
5258
"""The base class for all tools."""
5359

@@ -110,8 +116,8 @@ class BaseTool(ABC):
110116
def __init__(
111117
self,
112118
*,
113-
name,
114-
description,
119+
name: str,
120+
description: str,
115121
is_long_running: bool = False,
116122
custom_metadata: Optional[dict[str, Any]] = None,
117123
response_scheduling: Optional[types.FunctionResponseScheduling] = None,
@@ -235,21 +241,23 @@ def from_config(
235241
and value is not None
236242
):
237243
kwargs[param_name] = param_type.model_validate(value)
238-
elif param_type is Callable or get_origin(param_type) is Callable:
244+
elif _is_callable_annotation(param_type):
239245
kwargs[param_name] = config_agent_utils.resolve_fully_qualified_name(
240246
value
241247
)
242248
elif param_type in (list, set, dict):
243249
kwargs[param_name] = param_type(value)
244250
elif get_origin(param_type) is list:
245251
list_args = get_args(param_type)
246-
if issubclass(list_args[0], BaseModel):
252+
if inspect.isclass(list_args[0]) and issubclass(
253+
list_args[0], BaseModel
254+
):
247255
kwargs[param_name] = [
248256
list_args[0].model_validate(item) for item in value
249257
]
250258
elif list_args[0] in (int, str, bool, float):
251259
kwargs[param_name] = value
252-
elif list_args[0] is Callable or get_origin(list_args[0]) is Callable:
260+
elif _is_callable_annotation(list_args[0]):
253261
kwargs[param_name] = [
254262
config_agent_utils.resolve_fully_qualified_name(item)
255263
for item in value

src/google/adk/tools/base_toolset.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from abc import ABC
1818
from abc import abstractmethod
1919
import copy
20+
from typing import Callable
2021
from typing import final
2122
from typing import List
2223
from typing import Optional
@@ -27,6 +28,8 @@
2728
from typing import TypeVar
2829
from typing import Union
2930

31+
from google.genai import types
32+
3033
from ..agents.readonly_context import ReadonlyContext
3134
from ..auth.auth_tool import AuthConfig
3235
from .base_tool import BaseTool
@@ -146,10 +149,12 @@ async def get_tools_with_prefix(
146149
# Also update the function declaration name if the tool has one
147150
# Use default parameters to capture the current values in the closure
148151
def _create_prefixed_declaration(
149-
original_get_declaration=tool._get_declaration,
150-
prefixed_name=prefixed_name,
151-
):
152-
def _get_prefixed_declaration():
152+
original_get_declaration: Callable[
153+
[], Optional[types.FunctionDeclaration]
154+
] = tool._get_declaration,
155+
prefixed_name: str = prefixed_name,
156+
) -> Callable[[], Optional[types.FunctionDeclaration]]:
157+
def _get_prefixed_declaration() -> Optional[types.FunctionDeclaration]:
153158
declaration = original_get_declaration()
154159
if declaration is not None:
155160
declaration.name = prefixed_name
@@ -158,7 +163,9 @@ def _get_prefixed_declaration():
158163

159164
return _get_prefixed_declaration
160165

161-
tool_copy._get_declaration = _create_prefixed_declaration()
166+
tool_copy._get_declaration = ( # type: ignore[method-assign]
167+
_create_prefixed_declaration()
168+
)
162169
prefixed_tools.append(tool_copy)
163170

164171
self._cached_invocation_id = invocation_id

src/google/adk/tools/long_running_tool.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import Any
1718
from typing import Callable
1819
from typing import Optional
1920

@@ -40,7 +41,7 @@ class LongRunningFunctionTool(FunctionTool):
4041
is_long_running: Whether the tool is a long running operation.
4142
"""
4243

43-
def __init__(self, func: Callable):
44+
def __init__(self, func: Callable[..., Any]):
4445
super().__init__(func)
4546
self.is_long_running = True
4647

tests/unittests/tools/test_base_tool.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Any
16+
from typing import Callable
1517
from typing import Optional
18+
from typing import Union
1619

1720
from google.adk.agents.invocation_context import InvocationContext
1821
from google.adk.agents.sequential_agent import SequentialAgent
@@ -157,6 +160,54 @@ async def run_async(self, **kwargs):
157160
assert t2._defers_response is True
158161

159162

163+
def _sample_handler(value: str) -> str:
164+
return value
165+
166+
167+
def test_from_config_resolves_parametrized_callable():
168+
"""A ``Callable[..., ...]`` arg is resolved, not skipped as unsupported."""
169+
from google.adk.tools.tool_configs import ToolArgsConfig
170+
171+
class CallbackTool(BaseTool):
172+
173+
def __init__(self, handler: Callable[[str], str]):
174+
super().__init__(name='callback_tool', description='desc')
175+
self.handler = handler
176+
177+
async def run_async(self, **kwargs):
178+
pass
179+
180+
config = ToolArgsConfig(handler=f'{__name__}._sample_handler')
181+
tool = CallbackTool.from_config(config, '')
182+
183+
assert tool.handler is _sample_handler
184+
185+
186+
def test_from_config_skips_list_of_non_class():
187+
"""Non-class ``list`` element types are skipped, not an issubclass error."""
188+
from google.adk.tools.tool_configs import ToolArgsConfig
189+
190+
class ListTool(BaseTool):
191+
192+
def __init__(
193+
self,
194+
unions: Optional[list[Union[int, str]]] = None,
195+
anys: Optional[list[Any]] = None,
196+
):
197+
super().__init__(name='list_tool', description='desc')
198+
self.unions = unions
199+
self.anys = anys
200+
201+
async def run_async(self, **kwargs):
202+
pass
203+
204+
config = ToolArgsConfig(unions=[1, 'two'], anys=[1, 'two'])
205+
tool = ListTool.from_config(config, '')
206+
207+
assert tool.unions is None
208+
assert tool.anys is None
209+
210+
160211
def test_response_scheduling_defaults_to_none():
161212
"""response_scheduling defaults to None, preserving existing behavior."""
162213

0 commit comments

Comments
 (0)