google-adk 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. google/adk/a2a/__init__.py +13 -0
  2. google/adk/a2a/converters/__init__.py +13 -0
  3. google/adk/a2a/converters/part_converter.py +166 -0
  4. google/adk/agents/invocation_context.py +2 -0
  5. google/adk/agents/llm_agent.py +1 -6
  6. google/adk/agents/run_config.py +11 -0
  7. google/adk/auth/auth_credential.py +5 -0
  8. google/adk/auth/auth_handler.py +22 -96
  9. google/adk/auth/auth_preprocessor.py +3 -3
  10. google/adk/auth/auth_tool.py +46 -0
  11. google/adk/auth/credential_manager.py +265 -0
  12. google/adk/auth/credential_service/__init__.py +13 -0
  13. google/adk/auth/credential_service/base_credential_service.py +75 -0
  14. google/adk/auth/credential_service/in_memory_credential_service.py +64 -0
  15. google/adk/auth/exchanger/__init__.py +23 -0
  16. google/adk/auth/exchanger/base_credential_exchanger.py +57 -0
  17. google/adk/auth/exchanger/credential_exchanger_registry.py +58 -0
  18. google/adk/auth/exchanger/oauth2_credential_exchanger.py +104 -0
  19. google/adk/auth/exchanger/service_account_credential_exchanger.py +104 -0
  20. google/adk/auth/oauth2_credential_util.py +107 -0
  21. google/adk/auth/refresher/__init__.py +21 -0
  22. google/adk/auth/refresher/base_credential_refresher.py +74 -0
  23. google/adk/auth/refresher/credential_refresher_registry.py +59 -0
  24. google/adk/auth/refresher/oauth2_credential_refresher.py +154 -0
  25. google/adk/cli/agent_graph.py +34 -32
  26. google/adk/cli/browser/index.html +2 -2
  27. google/adk/cli/browser/main-JAAWEV7F.js +92 -0
  28. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  29. google/adk/cli/cli.py +10 -0
  30. google/adk/cli/cli_deploy.py +80 -21
  31. google/adk/cli/cli_tools_click.py +132 -61
  32. google/adk/cli/fast_api.py +46 -41
  33. google/adk/cli/utils/agent_loader.py +15 -2
  34. google/adk/cli/utils/evals.py +4 -2
  35. google/adk/code_executors/container_code_executor.py +10 -6
  36. google/adk/code_executors/vertex_ai_code_executor.py +8 -2
  37. google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
  38. google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
  39. google/adk/evaluation/eval_metrics.py +0 -5
  40. google/adk/evaluation/eval_result.py +12 -7
  41. google/adk/evaluation/eval_set_results_manager.py +6 -1
  42. google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
  43. google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
  44. google/adk/evaluation/local_eval_set_results_manager.py +6 -18
  45. google/adk/evaluation/local_eval_sets_manager.py +27 -78
  46. google/adk/evaluation/response_evaluator.py +5 -5
  47. google/adk/evaluation/trajectory_evaluator.py +9 -6
  48. google/adk/flows/llm_flows/basic.py +9 -0
  49. google/adk/models/anthropic_llm.py +1 -1
  50. google/adk/models/gemini_llm_connection.py +2 -0
  51. google/adk/models/google_llm.py +57 -16
  52. google/adk/models/lite_llm.py +2 -1
  53. google/adk/platform/__init__.py +13 -0
  54. google/adk/platform/internal/__init__.py +15 -0
  55. google/adk/platform/internal/thread.py +30 -0
  56. google/adk/platform/thread.py +31 -0
  57. google/adk/runners.py +8 -2
  58. google/adk/sessions/in_memory_session_service.py +12 -1
  59. google/adk/sessions/vertex_ai_session_service.py +71 -50
  60. google/adk/tools/__init__.py +2 -0
  61. google/adk/tools/_automatic_function_calling_util.py +1 -0
  62. google/adk/tools/_forwarding_artifact_service.py +96 -0
  63. google/adk/tools/_function_parameter_parse_util.py +1 -0
  64. google/adk/tools/agent_tool.py +5 -39
  65. google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
  66. google/adk/tools/authenticated_function_tool.py +107 -0
  67. google/adk/tools/base_authenticated_tool.py +107 -0
  68. google/adk/tools/bigquery/bigquery_credentials.py +6 -4
  69. google/adk/tools/bigquery/bigquery_tool.py +22 -9
  70. google/adk/tools/bigquery/bigquery_toolset.py +9 -3
  71. google/adk/tools/bigquery/client.py +7 -3
  72. google/adk/tools/bigquery/config.py +46 -0
  73. google/adk/tools/bigquery/metadata_tool.py +114 -91
  74. google/adk/tools/bigquery/query_tool.py +141 -23
  75. google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
  76. google/adk/tools/google_search_tool.py +0 -1
  77. google/adk/tools/mcp_tool/__init__.py +6 -0
  78. google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
  79. google/adk/tools/mcp_tool/mcp_tool.py +79 -22
  80. google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
  81. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
  82. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +56 -33
  83. google/adk/tools/retrieval/files_retrieval.py +7 -1
  84. google/adk/tools/url_context_tool.py +61 -0
  85. google/adk/tools/vertex_ai_search_tool.py +13 -2
  86. google/adk/utils/feature_decorator.py +175 -0
  87. google/adk/version.py +2 -2
  88. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/METADATA +10 -1
  89. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/RECORD +92 -61
  90. google/adk/cli/browser/main-CS5OLUMF.js +0 -91
  91. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
  92. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/WHEEL +0 -0
  93. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/entry_points.txt +0 -0
  94. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,13 +16,12 @@ from __future__ import annotations
16
16
  import asyncio
17
17
  import logging
18
18
  import re
19
- import time
20
19
  from typing import Any
20
+ from typing import Dict
21
21
  from typing import Optional
22
22
  import urllib.parse
23
23
 
24
24
  from dateutil import parser
25
- from google.genai import types
26
25
  from typing_extensions import override
27
26
 
28
27
  from google import genai
@@ -40,18 +39,27 @@ logger = logging.getLogger('google_adk.' + __name__)
40
39
 
41
40
 
42
41
  class VertexAiSessionService(BaseSessionService):
43
- """Connects to the managed Vertex AI Session Service."""
42
+ """Connects to the Vertex AI Agent Engine Session Service using GenAI API client.
43
+
44
+ https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/sessions/overview
45
+ """
44
46
 
45
47
  def __init__(
46
48
  self,
47
- project: str = None,
48
- location: str = None,
49
+ project: Optional[str] = None,
50
+ location: Optional[str] = None,
51
+ agent_engine_id: Optional[str] = None,
49
52
  ):
50
- self.project = project
51
- self.location = location
53
+ """Initializes the VertexAiSessionService.
52
54
 
53
- client = genai.Client(vertexai=True, project=project, location=location)
54
- self.api_client = client._api_client
55
+ Args:
56
+ project: The project id of the project to use.
57
+ location: The location of the project to use.
58
+ agent_engine_id: The resource ID of the agent engine to use.
59
+ """
60
+ self._project = project
61
+ self._location = location
62
+ self._agent_engine_id = agent_engine_id
55
63
 
56
64
  @override
57
65
  async def create_session(
@@ -67,14 +75,13 @@ class VertexAiSessionService(BaseSessionService):
67
75
  'User-provided Session id is not supported for'
68
76
  ' VertexAISessionService.'
69
77
  )
70
-
71
- reasoning_engine_id = _parse_reasoning_engine_id(app_name)
78
+ reasoning_engine_id = self._get_reasoning_engine_id(app_name)
79
+ api_client = self._get_api_client()
72
80
 
73
81
  session_json_dict = {'user_id': user_id}
74
82
  if state:
75
83
  session_json_dict['session_state'] = state
76
84
 
77
- api_client = _get_api_client(self.project, self.location)
78
85
  api_response = await api_client.async_request(
79
86
  http_method='POST',
80
87
  path=f'reasoningEngines/{reasoning_engine_id}/sessions',
@@ -86,6 +93,7 @@ class VertexAiSessionService(BaseSessionService):
86
93
  operation_id = api_response['name'].split('/')[-1]
87
94
 
88
95
  max_retry_attempt = 5
96
+ lro_response = None
89
97
  while max_retry_attempt >= 0:
90
98
  lro_response = await api_client.async_request(
91
99
  http_method='GET',
@@ -99,6 +107,11 @@ class VertexAiSessionService(BaseSessionService):
99
107
  await asyncio.sleep(1)
100
108
  max_retry_attempt -= 1
101
109
 
110
+ if lro_response is None or not lro_response.get('done', None):
111
+ raise TimeoutError(
112
+ f'Timeout waiting for operation {operation_id} to complete.'
113
+ )
114
+
102
115
  # Get session resource
103
116
  get_session_api_response = await api_client.async_request(
104
117
  http_method='GET',
@@ -127,10 +140,10 @@ class VertexAiSessionService(BaseSessionService):
127
140
  session_id: str,
128
141
  config: Optional[GetSessionConfig] = None,
129
142
  ) -> Optional[Session]:
130
- reasoning_engine_id = _parse_reasoning_engine_id(app_name)
143
+ reasoning_engine_id = self._get_reasoning_engine_id(app_name)
144
+ api_client = self._get_api_client()
131
145
 
132
146
  # Get session resource
133
- api_client = _get_api_client(self.project, self.location)
134
147
  get_session_api_response = await api_client.async_request(
135
148
  http_method='GET',
136
149
  path=f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}',
@@ -200,14 +213,14 @@ class VertexAiSessionService(BaseSessionService):
200
213
  async def list_sessions(
201
214
  self, *, app_name: str, user_id: str
202
215
  ) -> ListSessionsResponse:
203
- reasoning_engine_id = _parse_reasoning_engine_id(app_name)
216
+ reasoning_engine_id = self._get_reasoning_engine_id(app_name)
217
+ api_client = self._get_api_client()
204
218
 
205
219
  path = f'reasoningEngines/{reasoning_engine_id}/sessions'
206
220
  if user_id:
207
221
  parsed_user_id = urllib.parse.quote(f'''"{user_id}"''', safe='')
208
222
  path = path + f'?filter=user_id={parsed_user_id}'
209
223
 
210
- api_client = _get_api_client(self.project, self.location)
211
224
  api_response = await api_client.async_request(
212
225
  http_method='GET',
213
226
  path=path,
@@ -233,21 +246,26 @@ class VertexAiSessionService(BaseSessionService):
233
246
  async def delete_session(
234
247
  self, *, app_name: str, user_id: str, session_id: str
235
248
  ) -> None:
236
- reasoning_engine_id = _parse_reasoning_engine_id(app_name)
237
- api_client = _get_api_client(self.project, self.location)
238
- await api_client.async_request(
239
- http_method='DELETE',
240
- path=f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}',
241
- request_dict={},
242
- )
249
+ reasoning_engine_id = self._get_reasoning_engine_id(app_name)
250
+ api_client = self._get_api_client()
251
+
252
+ try:
253
+ await api_client.async_request(
254
+ http_method='DELETE',
255
+ path=f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}',
256
+ request_dict={},
257
+ )
258
+ except Exception as e:
259
+ logger.error(f'Error deleting session {session_id}: {e}')
260
+ raise e
243
261
 
244
262
  @override
245
263
  async def append_event(self, session: Session, event: Event) -> Event:
246
264
  # Update the in-memory session.
247
265
  await super().append_event(session=session, event=event)
248
266
 
249
- reasoning_engine_id = _parse_reasoning_engine_id(session.app_name)
250
- api_client = _get_api_client(self.project, self.location)
267
+ reasoning_engine_id = self._get_reasoning_engine_id(session.app_name)
268
+ api_client = self._get_api_client()
251
269
  await api_client.async_request(
252
270
  http_method='POST',
253
271
  path=f'reasoningEngines/{reasoning_engine_id}/sessions/{session.id}:appendEvent',
@@ -255,18 +273,37 @@ class VertexAiSessionService(BaseSessionService):
255
273
  )
256
274
  return event
257
275
 
276
+ def _get_reasoning_engine_id(self, app_name: str):
277
+ if self._agent_engine_id:
278
+ return self._agent_engine_id
258
279
 
259
- def _get_api_client(project: str, location: str):
260
- """Instantiates an API client for the given project and location.
280
+ if app_name.isdigit():
281
+ return app_name
261
282
 
262
- It needs to be instantiated inside each request so that the event loop
263
- management.
264
- """
265
- client = genai.Client(vertexai=True, project=project, location=location)
266
- return client._api_client
283
+ pattern = r'^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)$'
284
+ match = re.fullmatch(pattern, app_name)
285
+
286
+ if not bool(match):
287
+ raise ValueError(
288
+ f'App name {app_name} is not valid. It should either be the full'
289
+ ' ReasoningEngine resource name, or the reasoning engine id.'
290
+ )
291
+
292
+ return match.groups()[-1]
293
+
294
+ def _get_api_client(self):
295
+ """Instantiates an API client for the given project and location.
296
+
297
+ It needs to be instantiated inside each request so that the event loop
298
+ management can be properly propagated.
299
+ """
300
+ client = genai.Client(
301
+ vertexai=True, project=self._project, location=self._location
302
+ )
303
+ return client._api_client
267
304
 
268
305
 
269
- def _convert_event_to_json(event: Event):
306
+ def _convert_event_to_json(event: Event) -> Dict[str, Any]:
270
307
  metadata_json = {
271
308
  'partial': event.partial,
272
309
  'turn_complete': event.turn_complete,
@@ -318,7 +355,7 @@ def _convert_event_to_json(event: Event):
318
355
  return event_json
319
356
 
320
357
 
321
- def _from_api_event(api_event: dict) -> Event:
358
+ def _from_api_event(api_event: Dict[str, Any]) -> Event:
322
359
  event_actions = EventActions()
323
360
  if api_event.get('actions', None):
324
361
  event_actions = EventActions(
@@ -359,19 +396,3 @@ def _from_api_event(api_event: dict) -> Event:
359
396
  )
360
397
 
361
398
  return event
362
-
363
-
364
- def _parse_reasoning_engine_id(app_name: str):
365
- if app_name.isdigit():
366
- return app_name
367
-
368
- pattern = r'^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)$'
369
- match = re.fullmatch(pattern, app_name)
370
-
371
- if not bool(match):
372
- raise ValueError(
373
- f'App name {app_name} is not valid. It should either be the full'
374
- ' ReasoningEngine resource name, or the reasoning engine id.'
375
- )
376
-
377
- return match.groups()[-1]
@@ -27,6 +27,7 @@ from .long_running_tool import LongRunningFunctionTool
27
27
  from .preload_memory_tool import preload_memory_tool as preload_memory
28
28
  from .tool_context import ToolContext
29
29
  from .transfer_to_agent_tool import transfer_to_agent
30
+ from .url_context_tool import url_context
30
31
  from .vertex_ai_search_tool import VertexAiSearchTool
31
32
 
32
33
  __all__ = [
@@ -34,6 +35,7 @@ __all__ = [
34
35
  'AuthToolArguments',
35
36
  'BaseTool',
36
37
  'google_search',
38
+ 'url_context',
37
39
  'VertexAiSearchTool',
38
40
  'ExampleTool',
39
41
  'exit_loop',
@@ -230,6 +230,7 @@ def build_function_declaration(
230
230
  )
231
231
  new_func.__signature__ = new_sig
232
232
  new_func.__doc__ = func.__doc__
233
+ new_func.__annotations__ = func.__annotations__
233
234
 
234
235
  return (
235
236
  from_function_with_options(func, variant)
@@ -0,0 +1,96 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+ from typing import TYPE_CHECKING
19
+
20
+ from google.genai import types
21
+ from typing_extensions import override
22
+
23
+ from ..artifacts.base_artifact_service import BaseArtifactService
24
+
25
+ if TYPE_CHECKING:
26
+ from .tool_context import ToolContext
27
+
28
+
29
+ class ForwardingArtifactService(BaseArtifactService):
30
+ """Artifact service that forwards to the parent tool context."""
31
+
32
+ def __init__(self, tool_context: ToolContext):
33
+ self.tool_context = tool_context
34
+ self._invocation_context = tool_context._invocation_context
35
+
36
+ @override
37
+ async def save_artifact(
38
+ self,
39
+ *,
40
+ app_name: str,
41
+ user_id: str,
42
+ session_id: str,
43
+ filename: str,
44
+ artifact: types.Part,
45
+ ) -> int:
46
+ return await self.tool_context.save_artifact(
47
+ filename=filename, artifact=artifact
48
+ )
49
+
50
+ @override
51
+ async def load_artifact(
52
+ self,
53
+ *,
54
+ app_name: str,
55
+ user_id: str,
56
+ session_id: str,
57
+ filename: str,
58
+ version: Optional[int] = None,
59
+ ) -> Optional[types.Part]:
60
+ return await self.tool_context.load_artifact(
61
+ filename=filename, version=version
62
+ )
63
+
64
+ @override
65
+ async def list_artifact_keys(
66
+ self, *, app_name: str, user_id: str, session_id: str
67
+ ) -> list[str]:
68
+ return await self.tool_context.list_artifacts()
69
+
70
+ @override
71
+ async def delete_artifact(
72
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
73
+ ) -> None:
74
+ del app_name, user_id, session_id
75
+ if self._invocation_context.artifact_service is None:
76
+ raise ValueError("Artifact service is not initialized.")
77
+ await self._invocation_context.artifact_service.delete_artifact(
78
+ app_name=self._invocation_context.app_name,
79
+ user_id=self._invocation_context.user_id,
80
+ session_id=self._invocation_context.session.id,
81
+ filename=filename,
82
+ )
83
+
84
+ @override
85
+ async def list_versions(
86
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
87
+ ) -> list[int]:
88
+ del app_name, user_id, session_id
89
+ if self._invocation_context.artifact_service is None:
90
+ raise ValueError("Artifact service is not initialized.")
91
+ return await self._invocation_context.artifact_service.list_versions(
92
+ app_name=self._invocation_context.app_name,
93
+ user_id=self._invocation_context.user_id,
94
+ session_id=self._invocation_context.session.id,
95
+ filename=filename,
96
+ )
@@ -37,6 +37,7 @@ _py_builtin_type_to_schema_type = {
37
37
  bool: types.Type.BOOLEAN,
38
38
  list: types.Type.ARRAY,
39
39
  dict: types.Type.OBJECT,
40
+ None: types.Type.NULL,
40
41
  }
41
42
 
42
43
  logger = logging.getLogger('google_adk.' + __name__)
@@ -25,6 +25,7 @@ from . import _automatic_function_calling_util
25
25
  from ..memory.in_memory_memory_service import InMemoryMemoryService
26
26
  from ..runners import Runner
27
27
  from ..sessions.in_memory_session_service import InMemorySessionService
28
+ from ._forwarding_artifact_service import ForwardingArtifactService
28
29
  from .base_tool import BaseTool
29
30
  from .tool_context import ToolContext
30
31
 
@@ -96,17 +97,6 @@ class AgentTool(BaseTool):
96
97
 
97
98
  if isinstance(self.agent, LlmAgent) and self.agent.input_schema:
98
99
  input_value = self.agent.input_schema.model_validate(args)
99
- else:
100
- input_value = args['request']
101
-
102
- if isinstance(self.agent, LlmAgent) and self.agent.input_schema:
103
- if isinstance(input_value, dict):
104
- input_value = self.agent.input_schema.model_validate(input_value)
105
- if not isinstance(input_value, self.agent.input_schema):
106
- raise ValueError(
107
- f'Input value {input_value} is not of type'
108
- f' `{self.agent.input_schema}`.'
109
- )
110
100
  content = types.Content(
111
101
  role='user',
112
102
  parts=[
@@ -118,14 +108,12 @@ class AgentTool(BaseTool):
118
108
  else:
119
109
  content = types.Content(
120
110
  role='user',
121
- parts=[types.Part.from_text(text=input_value)],
111
+ parts=[types.Part.from_text(text=args['request'])],
122
112
  )
123
113
  runner = Runner(
124
114
  app_name=self.agent.name,
125
115
  agent=self.agent,
126
- # TODO(kech): Remove the access to the invocation context.
127
- # It seems we don't need re-use artifact_service if we forward below.
128
- artifact_service=tool_context._invocation_context.artifact_service,
116
+ artifact_service=ForwardingArtifactService(tool_context),
129
117
  session_service=InMemorySessionService(),
130
118
  memory_service=InMemoryMemoryService(),
131
119
  )
@@ -144,35 +132,13 @@ class AgentTool(BaseTool):
144
132
  tool_context.state.update(event.actions.state_delta)
145
133
  last_event = event
146
134
 
147
- if runner.artifact_service:
148
- # Forward all artifacts to parent session.
149
- artifact_names = await runner.artifact_service.list_artifact_keys(
150
- app_name=session.app_name,
151
- user_id=session.user_id,
152
- session_id=session.id,
153
- )
154
- for artifact_name in artifact_names:
155
- if artifact := await runner.artifact_service.load_artifact(
156
- app_name=session.app_name,
157
- user_id=session.user_id,
158
- session_id=session.id,
159
- filename=artifact_name,
160
- ):
161
- await tool_context.save_artifact(
162
- filename=artifact_name, artifact=artifact
163
- )
164
-
165
135
  if not last_event or not last_event.content or not last_event.content.parts:
166
136
  return ''
137
+ merged_text = '\n'.join(p.text for p in last_event.content.parts if p.text)
167
138
  if isinstance(self.agent, LlmAgent) and self.agent.output_schema:
168
- merged_text = '\n'.join(
169
- [p.text for p in last_event.content.parts if p.text]
170
- )
171
139
  tool_result = self.agent.output_schema.model_validate_json(
172
140
  merged_text
173
141
  ).model_dump(exclude_none=True)
174
142
  else:
175
- tool_result = '\n'.join(
176
- [p.text for p in last_event.content.parts if p.text]
177
- )
143
+ tool_result = merged_text
178
144
  return tool_result
@@ -150,7 +150,7 @@ class IntegrationConnectorTool(BaseTool):
150
150
  tool_auth_handler = ToolAuthHandler.from_tool_context(
151
151
  tool_context, self._auth_scheme, self._auth_credential
152
152
  )
153
- auth_result = tool_auth_handler.prepare_auth_credentials()
153
+ auth_result = await tool_auth_handler.prepare_auth_credentials()
154
154
 
155
155
  if auth_result.state == 'pending':
156
156
  return {
@@ -178,7 +178,7 @@ class IntegrationConnectorTool(BaseTool):
178
178
  args['operation'] = self._operation
179
179
  args['action'] = self._action
180
180
  logger.info('Running tool: %s with args: %s', self.name, args)
181
- return self._rest_api_tool.call(args=args, tool_context=tool_context)
181
+ return await self._rest_api_tool.call(args=args, tool_context=tool_context)
182
182
 
183
183
  def __str__(self):
184
184
  return (
@@ -0,0 +1,107 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import inspect
18
+ import logging
19
+ from typing import Any
20
+ from typing import Callable
21
+ from typing import Dict
22
+ from typing import Optional
23
+ from typing import Union
24
+
25
+ from typing_extensions import override
26
+
27
+ from ..auth.auth_credential import AuthCredential
28
+ from ..auth.auth_tool import AuthConfig
29
+ from ..auth.credential_manager import CredentialManager
30
+ from ..utils.feature_decorator import experimental
31
+ from .function_tool import FunctionTool
32
+ from .tool_context import ToolContext
33
+
34
+ logger = logging.getLogger("google_adk." + __name__)
35
+
36
+
37
+ @experimental
38
+ class AuthenticatedFunctionTool(FunctionTool):
39
+ """A FunctionTool that handles authentication before the actual tool logic
40
+ gets called. Functions can accept a special `credential` argument which is the
41
+ credential ready for use.(Experimental)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ func: Callable[..., Any],
48
+ auth_config: AuthConfig = None,
49
+ response_for_auth_required: Optional[Union[dict[str, Any], str]] = None,
50
+ ):
51
+ """Initializes the AuthenticatedFunctionTool.
52
+
53
+ Args:
54
+ func: The function to be called.
55
+ auth_config: The authentication configuration.
56
+ response_for_auth_required: The response to return when the tool is
57
+ requesting auth credential from the client. There could be two case,
58
+ the tool doesn't configure any credentials
59
+ (auth_config.raw_auth_credential is missing) or the credentials
60
+ configured is not enough to authenticate the tool (e.g. an OAuth
61
+ client id and client secrect is configured.) and needs client input
62
+ (e.g. client need to involve the end user in an oauth flow and get
63
+ back the oauth response.)
64
+ """
65
+ super().__init__(func=func)
66
+ self._ignore_params.append("credential")
67
+
68
+ if auth_config and auth_config.auth_scheme:
69
+ self._credentials_manager = CredentialManager(auth_config=auth_config)
70
+ else:
71
+ logger.warning(
72
+ "auth_config or auth_config.auth_scheme is missing. Will skip"
73
+ " authentication.Using FunctionTool instead if authentication is not"
74
+ " required."
75
+ )
76
+ self._credentials_manager = None
77
+ self._response_for_auth_required = response_for_auth_required
78
+
79
+ @override
80
+ async def run_async(
81
+ self, *, args: dict[str, Any], tool_context: ToolContext
82
+ ) -> Any:
83
+ credential = None
84
+ if self._credentials_manager:
85
+ credential = await self._credentials_manager.get_auth_credential(
86
+ tool_context
87
+ )
88
+ if not credential:
89
+ await self._credentials_manager.request_credential(tool_context)
90
+ return self._response_for_auth_required or "Pending User Authorization."
91
+
92
+ return await self._run_async_impl(
93
+ args=args, tool_context=tool_context, credential=credential
94
+ )
95
+
96
+ async def _run_async_impl(
97
+ self,
98
+ *,
99
+ args: dict[str, Any],
100
+ tool_context: ToolContext,
101
+ credential: AuthCredential,
102
+ ) -> Any:
103
+ args_to_call = args.copy()
104
+ signature = inspect.signature(self.func)
105
+ if "credential" in signature.parameters:
106
+ args_to_call["credential"] = credential
107
+ return await super().run_async(args=args_to_call, tool_context=tool_context)
@@ -0,0 +1,107 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from abc import abstractmethod
18
+ import logging
19
+ from typing import Any
20
+ from typing import Optional
21
+ from typing import Union
22
+
23
+ from typing_extensions import override
24
+
25
+ from ..auth.auth_credential import AuthCredential
26
+ from ..auth.auth_tool import AuthConfig
27
+ from ..auth.credential_manager import CredentialManager
28
+ from ..utils.feature_decorator import experimental
29
+ from .base_tool import BaseTool
30
+ from .tool_context import ToolContext
31
+
32
+ logger = logging.getLogger("google_adk." + __name__)
33
+
34
+
35
+ @experimental
36
+ class BaseAuthenticatedTool(BaseTool):
37
+ """A base tool class that handles authentication before the actual tool logic
38
+ gets called. Functions can accept a special `credential` argument which is the
39
+ credential ready for use.(Experimental)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ name,
46
+ description,
47
+ auth_config: AuthConfig = None,
48
+ response_for_auth_required: Optional[Union[dict[str, Any], str]] = None,
49
+ ):
50
+ """
51
+ Args:
52
+ name: The name of the tool.
53
+ description: The description of the tool.
54
+ auth_config: The auth configuration of the tool.
55
+ response_for_auth_required: The response to return when the tool is
56
+ requesting auth credential from the client. There could be two case,
57
+ the tool doesn't configure any credentials
58
+ (auth_config.raw_auth_credential is missing) or the credentials
59
+ configured is not enough to authenticate the tool (e.g. an OAuth
60
+ client id and client secrect is configured.) and needs client input
61
+ (e.g. client need to involve the end user in an oauth flow and get
62
+ back the oauth response.)
63
+ """
64
+ super().__init__(
65
+ name=name,
66
+ description=description,
67
+ )
68
+
69
+ if auth_config and auth_config.auth_scheme:
70
+ self._credentials_manager = CredentialManager(auth_config=auth_config)
71
+ else:
72
+ logger.warning(
73
+ "auth_config or auth_config.auth_scheme is missing. Will skip"
74
+ " authentication.Using FunctionTool instead if authentication is not"
75
+ " required."
76
+ )
77
+ self._credentials_manager = None
78
+ self._response_for_auth_required = response_for_auth_required
79
+
80
+ @override
81
+ async def run_async(
82
+ self, *, args: dict[str, Any], tool_context: ToolContext
83
+ ) -> Any:
84
+ credential = None
85
+ if self._credentials_manager:
86
+ credential = await self._credentials_manager.get_auth_credential(
87
+ tool_context
88
+ )
89
+ if not credential:
90
+ await self._credentials_manager.request_credential(tool_context)
91
+ return self._response_for_auth_required or "Pending User Authorization."
92
+
93
+ return await self._run_async_impl(
94
+ args=args,
95
+ tool_context=tool_context,
96
+ credential=credential,
97
+ )
98
+
99
+ @abstractmethod
100
+ async def _run_async_impl(
101
+ self,
102
+ *,
103
+ args: dict[str, Any],
104
+ tool_context: ToolContext,
105
+ credential: AuthCredential,
106
+ ) -> Any:
107
+ pass