Skip to content

Commit 8499fd8

Browse files
GWealecopybara-github
authored andcommitted
refactor(types): type the computer use, data agent, retrieval and Google API tools for strict mypy
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 970121611
1 parent b0c599f commit 8499fd8

9 files changed

Lines changed: 160 additions & 74 deletions

File tree

src/google/adk/tools/computer_use/computer_use_toolset.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import logging
2121
from typing import Any
2222
from typing import Callable
23+
from typing import cast
2324
from typing import Optional
2425
from typing import Union
2526

@@ -72,7 +73,7 @@ def __init__(
7273
self._excluded_predefined_functions = excluded_predefined_functions
7374
self._allow_private_network_access = allow_private_network_access
7475
self._initialized = False
75-
self._tools = None
76+
self._tools: Optional[list[ComputerUseTool]] = None
7677

7778
async def _ensure_initialized(self) -> None:
7879
if not self._initialized:
@@ -99,7 +100,9 @@ def _wrap_method_with_state_binding(
99100

100101
@functools.wraps(method)
101102
async def wrapper(
102-
*args: Any, tool_context: ToolContext = None, **kwargs: Any
103+
*args: Any,
104+
tool_context: Optional[ToolContext] = None,
105+
**kwargs: Any,
103106
) -> Any:
104107
# Prepare computer before each tool call
105108
# Computers that need session state (e.g., AgentEngineSandboxComputer)
@@ -121,7 +124,7 @@ async def wrapper(
121124
annotation=ToolContext,
122125
)
123126
]
124-
wrapper.__signature__ = orig_sig.replace(parameters=new_params)
127+
setattr(wrapper, "__signature__", orig_sig.replace(parameters=new_params))
125128

126129
return wrapper
127130

@@ -200,16 +203,14 @@ async def adapt_computer_use_tool(
200203
logger.warning("Method %s not found in tools_dict", method_name)
201204
return
202205

203-
original_tool = llm_request.tools_dict[method_name]
206+
original_tool = cast(ComputerUseTool, llm_request.tools_dict[method_name])
204207

205208
# Create the adapted function using the adapter
206-
# Handle both sync and async adapter functions
207-
if asyncio.iscoroutinefunction(adapter_func):
208-
# If adapter_func is async, await it to get the adapted function
209-
adapted_func = await adapter_func(original_tool.func)
209+
adapted_func_or_awaitable = adapter_func(original_tool.func)
210+
if inspect.isawaitable(adapted_func_or_awaitable):
211+
adapted_func = await adapted_func_or_awaitable
210212
else:
211-
# If adapter_func is sync, call it directly
212-
adapted_func = adapter_func(original_tool.func)
213+
adapted_func = adapted_func_or_awaitable
213214

214215
# Get the name from the adapted function
215216
new_method_name = adapted_func.__name__
@@ -232,7 +233,9 @@ async def adapt_computer_use_tool(
232233
)
233234

234235
@override
235-
async def get_tools(
236+
# list is invariant, so the narrower element type is not a compatible
237+
# override; widening it to BaseTool would change this public signature.
238+
async def get_tools( # type: ignore[override]
236239
self,
237240
readonly_context: Optional[ReadonlyContext] = None,
238241
) -> list[ComputerUseTool]:
@@ -306,16 +309,20 @@ async def process_llm_request(
306309
if not self._tools:
307310
await self.get_tools()
308311

309-
for tool in self._tools:
310-
llm_request.tools_dict[tool.name] = tool
312+
assert self._tools is not None
313+
for computer_tool in self._tools:
314+
llm_request.tools_dict[computer_tool.name] = computer_tool
311315

312316
# Initialize config if needed
313317
llm_request.config = llm_request.config or types.GenerateContentConfig()
314318
llm_request.config.tools = llm_request.config.tools or []
315319

316320
# Check if computer use is already configured
317-
for tool in llm_request.config.tools:
318-
if isinstance(tool, types.Tool) and tool.computer_use:
321+
for configured_tool in llm_request.config.tools:
322+
if (
323+
isinstance(configured_tool, types.Tool)
324+
and configured_tool.computer_use
325+
):
319326
logger.debug("Computer use already configured in LLM request")
320327
return
321328

src/google/adk/tools/data_agent/credentials.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class DataAgentCredentialsConfig(BaseGoogleCredentialsConfig):
2525

2626
def __post_init__(self) -> DataAgentCredentialsConfig:
2727
"""Populate default scope if scopes is None."""
28-
super().__post_init__()
28+
# pydantic wraps the base @model_validator in a descriptor proxy that mypy
29+
# does not treat as callable; it binds to the function normally at runtime.
30+
super().__post_init__() # type: ignore[operator]
2931

3032
if not self.scopes:
3133
self.scopes = DATA_AGENT_DEFAULT_SCOPE

src/google/adk/tools/data_agent/data_agent_toolset.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import Any
18+
from typing import Callable
1719
from typing import List
18-
from typing import Optional
19-
from typing import Union
2020

2121
from google.adk.agents.readonly_context import ReadonlyContext
2222
from typing_extensions import override
@@ -36,9 +36,9 @@ class DataAgentToolset(BaseToolset):
3636
def __init__(
3737
self,
3838
*,
39-
tool_filter: Optional[Union[ToolPredicate, List[str]]] = None,
40-
credentials_config: Optional[DataAgentCredentialsConfig] = None,
41-
data_agent_tool_config: Optional[DataAgentToolConfig] = None,
39+
tool_filter: ToolPredicate | list[str] | None = None,
40+
credentials_config: DataAgentCredentialsConfig | None = None,
41+
data_agent_tool_config: DataAgentToolConfig | None = None,
4242
):
4343
super().__init__(tool_filter=tool_filter)
4444
self._credentials_config = credentials_config
@@ -49,8 +49,9 @@ def __init__(
4949
)
5050

5151
def _is_tool_selected(
52-
self, tool: BaseTool, readonly_context: ReadonlyContext
52+
self, tool: BaseTool, readonly_context: ReadonlyContext | None
5353
) -> bool:
54+
# Unlike the base implementation, an empty tool_filter selects no tools.
5455
if self.tool_filter is None:
5556
return True
5657

@@ -64,9 +65,9 @@ def _is_tool_selected(
6465

6566
@override
6667
async def get_tools(
67-
self, readonly_context: Optional[ReadonlyContext] = None
68+
self, readonly_context: ReadonlyContext | None = None
6869
) -> List[BaseTool]:
69-
funcs = [
70+
funcs: list[Callable[..., Any]] = [
7071
data_agent_tool.list_accessible_data_agents,
7172
data_agent_tool.get_data_agent_info,
7273
data_agent_tool.ask_data_agent,
@@ -92,5 +93,5 @@ async def get_tools(
9293
]
9394

9495
@override
95-
async def close(self):
96+
async def close(self) -> None:
9697
pass

src/google/adk/tools/google_api_tool/google_api_toolset.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
from __future__ import annotations
1616

1717
import logging
18+
from typing import Callable
1819
from typing import Dict
1920
from typing import List
20-
from typing import Optional
21-
from typing import Union
2221

2322
import httpx
2423
from typing_extensions import override
@@ -62,15 +61,15 @@ def __init__(
6261
self,
6362
api_name: str,
6463
api_version: str,
65-
client_id: Optional[str] = None,
66-
client_secret: Optional[str] = None,
67-
tool_filter: Optional[Union[ToolPredicate, List[str]]] = None,
68-
service_account: Optional[ServiceAccount] = None,
69-
tool_name_prefix: Optional[str] = None,
64+
client_id: str | None = None,
65+
client_secret: str | None = None,
66+
tool_filter: ToolPredicate | List[str] | None = None,
67+
service_account: ServiceAccount | None = None,
68+
tool_name_prefix: str | None = None,
7069
*,
71-
additional_headers: Optional[Dict[str, str]] = None,
72-
additional_scopes: Optional[List[str]] = None,
73-
discovery_url: Optional[str] = None,
70+
additional_headers: Dict[str, str] | None = None,
71+
additional_scopes: List[str] | None = None,
72+
discovery_url: str | None = None,
7473
):
7574
super().__init__(tool_filter=tool_filter, tool_name_prefix=tool_name_prefix)
7675
self.api_name = api_name
@@ -82,7 +81,8 @@ def __init__(
8281
self._additional_scopes = additional_scopes
8382
self._discovery_url = discovery_url
8483

85-
self._httpx_client_factory = None
84+
self._httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None
85+
self._mtls_certs: MtlsClientCerts | None = None
8686
use_client_cert = use_client_cert_effective()
8787

8888
if use_client_cert:
@@ -102,8 +102,10 @@ def client_factory() -> httpx.AsyncClient:
102102
self._openapi_toolset = self._load_toolset_with_oidc_auth()
103103

104104
@override
105-
async def get_tools(
106-
self, readonly_context: Optional[ReadonlyContext] = None
105+
# list is invariant, so the narrower element type is not a compatible
106+
# override; widening it to BaseTool would change this public signature.
107+
async def get_tools( # type: ignore[override]
108+
self, readonly_context: ReadonlyContext | None = None
107109
) -> List[GoogleApiTool]:
108110
"""Get all tools in the toolset."""
109111
return [
@@ -118,9 +120,7 @@ async def get_tools(
118120
if self._is_tool_selected(tool, readonly_context)
119121
]
120122

121-
def set_tool_filter(
122-
self, tool_filter: Union[ToolPredicate, List[str]]
123-
) -> None:
123+
def set_tool_filter(self, tool_filter: ToolPredicate | List[str]) -> None:
124124
self.tool_filter = tool_filter
125125

126126
def _load_toolset_with_oidc_auth(self) -> OpenAPIToolset:
@@ -171,5 +171,5 @@ def configure_sa_auth(self, service_account: ServiceAccount) -> None:
171171
async def close(self) -> None:
172172
if self._openapi_toolset:
173173
await self._openapi_toolset.close()
174-
if hasattr(self, '_mtls_certs') and self._mtls_certs:
174+
if self._mtls_certs:
175175
self._mtls_certs.close()

src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from __future__ import annotations
1616

1717
import argparse
18+
from collections.abc import Mapping
1819
import json
1920
import logging
2021
import socket
2122
from typing import Any
2223
from typing import Dict
23-
from typing import List
2424

2525
# Google API client
2626
from googleapiclient.discovery import build
@@ -50,9 +50,11 @@ def __init__(
5050
self._api_name = api_name
5151
self._api_version = api_version
5252
self._discovery_url = discovery_url
53-
self._google_api_resource = None
54-
self._google_api_spec = None
55-
self._openapi_spec = {
53+
self._google_api_resource: object | None = None
54+
# Discovery documents are heterogeneous JSON objects, and this attribute
55+
# is only populated once the document has been fetched.
56+
self._google_api_spec: Any = None
57+
self._openapi_spec: dict[str, Any] = {
5658
"openapi": "3.0.0",
5759
"info": {},
5860
"servers": [],
@@ -108,10 +110,12 @@ def fetch_google_api_spec(self) -> None:
108110
)
109111

110112
# Access the underlying API discovery document
111-
self._google_api_spec = self._google_api_resource._rootDesc
112-
113-
if not self._google_api_spec:
113+
root_desc = getattr(self._google_api_resource, "_rootDesc", None)
114+
if not isinstance(root_desc, dict) or not root_desc:
114115
raise ValueError("Failed to retrieve API specification")
116+
if not all(isinstance(key, str) for key in root_desc):
117+
raise ValueError("API specification keys must be strings")
118+
self._google_api_spec = root_desc
115119

116120
logger.info("Successfully fetched %s API specification", self._api_name)
117121
except HttpError as e:
@@ -200,7 +204,7 @@ def _convert_security_schemes(self) -> None:
200204
if oauth2:
201205
# Handle OAuth2
202206
scopes = oauth2.get("scopes", {})
203-
formatted_scopes = {}
207+
formatted_scopes: dict[str, str] = {}
204208

205209
for scope, scope_info in scopes.items():
206210
formatted_scopes[scope] = scope_info.get("description", "")
@@ -244,8 +248,8 @@ def _convert_schemas(self) -> None:
244248
] = converted_schema
245249

246250
def _convert_schema_object(
247-
self, schema_def: Dict[str, Any]
248-
) -> Dict[str, Any]:
251+
self, schema_def: Mapping[str, Any]
252+
) -> dict[str, Any]:
249253
"""Recursively convert a Google API schema object to OpenAPI schema.
250254
251255
Args:
@@ -254,7 +258,7 @@ def _convert_schema_object(
254258
Returns:
255259
Converted OpenAPI schema object
256260
"""
257-
result = {}
261+
result: dict[str, Any] = {}
258262

259263
# Convert the type
260264
if "type" in schema_def:
@@ -332,7 +336,7 @@ def _convert_schema_object(
332336
return result
333337

334338
def _convert_resources(
335-
self, resources: Dict[str, Any], parent_path: str = ""
339+
self, resources: Mapping[str, Any], parent_path: str = ""
336340
) -> None:
337341
"""Recursively convert all resources and their methods.
338342
@@ -352,7 +356,7 @@ def _convert_resources(
352356
self._convert_resources(nested_resources, resource_path)
353357

354358
def _convert_methods(
355-
self, methods: Dict[str, Any], resource_path: str
359+
self, methods: Mapping[str, Any], resource_path: str
356360
) -> None:
357361
"""Convert methods for a specific resource path.
358362
@@ -382,7 +386,7 @@ def _convert_methods(
382386
self._convert_operation(method_data, path_params)
383387
)
384388

385-
def _extract_path_parameters(self, path: str) -> List[str]:
389+
def _extract_path_parameters(self, path: str) -> list[str]:
386390
"""Extract path parameters from a URL path.
387391
388392
Args:
@@ -403,8 +407,8 @@ def _extract_path_parameters(self, path: str) -> List[str]:
403407
return params
404408

405409
def _convert_operation(
406-
self, method_data: Dict[str, Any], path_params: List[str]
407-
) -> Dict[str, Any]:
410+
self, method_data: Mapping[str, Any], path_params: list[str]
411+
) -> dict[str, Any]:
408412
"""Convert a Google API method to an OpenAPI operation.
409413
410414
Args:
@@ -414,7 +418,7 @@ def _convert_operation(
414418
Returns:
415419
OpenAPI operation object
416420
"""
417-
operation = {
421+
operation: dict[str, Any] = {
418422
"operationId": method_data.get("id", ""),
419423
"summary": method_data.get("description", ""),
420424
"description": method_data.get("description", ""),
@@ -491,8 +495,8 @@ def _convert_operation(
491495
return operation
492496

493497
def _convert_parameter_schema(
494-
self, param_data: Dict[str, Any]
495-
) -> Dict[str, Any]:
498+
self, param_data: Mapping[str, Any]
499+
) -> dict[str, Any]:
496500
"""Convert a parameter definition to an OpenAPI schema.
497501
498502
Args:
@@ -501,7 +505,7 @@ def _convert_parameter_schema(
501505
Returns:
502506
OpenAPI schema for the parameter
503507
"""
504-
schema = {}
508+
schema: dict[str, Any] = {}
505509

506510
# Convert type
507511
param_type = param_data.get("type", "string")
@@ -536,7 +540,7 @@ def save_openapi_spec(self, output_path: str) -> None:
536540
logger.info("OpenAPI specification saved to %s", output_path)
537541

538542

539-
def main():
543+
def main() -> int:
540544
"""Command line interface for the converter."""
541545
parser = argparse.ArgumentParser(
542546
description=(
@@ -575,4 +579,4 @@ def main():
575579

576580

577581
if __name__ == "__main__":
578-
main()
582+
raise SystemExit(main())

0 commit comments

Comments
 (0)