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.
- google/adk/a2a/__init__.py +13 -0
- google/adk/a2a/converters/__init__.py +13 -0
- google/adk/a2a/converters/part_converter.py +166 -0
- google/adk/agents/invocation_context.py +2 -0
- google/adk/agents/llm_agent.py +1 -6
- google/adk/agents/run_config.py +11 -0
- google/adk/auth/auth_credential.py +5 -0
- google/adk/auth/auth_handler.py +22 -96
- google/adk/auth/auth_preprocessor.py +3 -3
- google/adk/auth/auth_tool.py +46 -0
- google/adk/auth/credential_manager.py +265 -0
- google/adk/auth/credential_service/__init__.py +13 -0
- google/adk/auth/credential_service/base_credential_service.py +75 -0
- google/adk/auth/credential_service/in_memory_credential_service.py +64 -0
- google/adk/auth/exchanger/__init__.py +23 -0
- google/adk/auth/exchanger/base_credential_exchanger.py +57 -0
- google/adk/auth/exchanger/credential_exchanger_registry.py +58 -0
- google/adk/auth/exchanger/oauth2_credential_exchanger.py +104 -0
- google/adk/auth/exchanger/service_account_credential_exchanger.py +104 -0
- google/adk/auth/oauth2_credential_util.py +107 -0
- google/adk/auth/refresher/__init__.py +21 -0
- google/adk/auth/refresher/base_credential_refresher.py +74 -0
- google/adk/auth/refresher/credential_refresher_registry.py +59 -0
- google/adk/auth/refresher/oauth2_credential_refresher.py +154 -0
- google/adk/cli/agent_graph.py +34 -32
- google/adk/cli/browser/index.html +2 -2
- google/adk/cli/browser/main-JAAWEV7F.js +92 -0
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
- google/adk/cli/cli.py +10 -0
- google/adk/cli/cli_deploy.py +80 -21
- google/adk/cli/cli_tools_click.py +132 -61
- google/adk/cli/fast_api.py +46 -41
- google/adk/cli/utils/agent_loader.py +15 -2
- google/adk/cli/utils/evals.py +4 -2
- google/adk/code_executors/container_code_executor.py +10 -6
- google/adk/code_executors/vertex_ai_code_executor.py +8 -2
- google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
- google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
- google/adk/evaluation/eval_metrics.py +0 -5
- google/adk/evaluation/eval_result.py +12 -7
- google/adk/evaluation/eval_set_results_manager.py +6 -1
- google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
- google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
- google/adk/evaluation/local_eval_set_results_manager.py +6 -18
- google/adk/evaluation/local_eval_sets_manager.py +27 -78
- google/adk/evaluation/response_evaluator.py +5 -5
- google/adk/evaluation/trajectory_evaluator.py +9 -6
- google/adk/flows/llm_flows/basic.py +9 -0
- google/adk/models/anthropic_llm.py +1 -1
- google/adk/models/gemini_llm_connection.py +2 -0
- google/adk/models/google_llm.py +57 -16
- google/adk/models/lite_llm.py +2 -1
- google/adk/platform/__init__.py +13 -0
- google/adk/platform/internal/__init__.py +15 -0
- google/adk/platform/internal/thread.py +30 -0
- google/adk/platform/thread.py +31 -0
- google/adk/runners.py +8 -2
- google/adk/sessions/in_memory_session_service.py +12 -1
- google/adk/sessions/vertex_ai_session_service.py +71 -50
- google/adk/tools/__init__.py +2 -0
- google/adk/tools/_automatic_function_calling_util.py +1 -0
- google/adk/tools/_forwarding_artifact_service.py +96 -0
- google/adk/tools/_function_parameter_parse_util.py +1 -0
- google/adk/tools/agent_tool.py +5 -39
- google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
- google/adk/tools/authenticated_function_tool.py +107 -0
- google/adk/tools/base_authenticated_tool.py +107 -0
- google/adk/tools/bigquery/bigquery_credentials.py +6 -4
- google/adk/tools/bigquery/bigquery_tool.py +22 -9
- google/adk/tools/bigquery/bigquery_toolset.py +9 -3
- google/adk/tools/bigquery/client.py +7 -3
- google/adk/tools/bigquery/config.py +46 -0
- google/adk/tools/bigquery/metadata_tool.py +114 -91
- google/adk/tools/bigquery/query_tool.py +141 -23
- google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
- google/adk/tools/google_search_tool.py +0 -1
- google/adk/tools/mcp_tool/__init__.py +6 -0
- google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
- google/adk/tools/mcp_tool/mcp_tool.py +79 -22
- google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
- google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +56 -33
- google/adk/tools/retrieval/files_retrieval.py +7 -1
- google/adk/tools/url_context_tool.py +61 -0
- google/adk/tools/vertex_ai_search_tool.py +13 -2
- google/adk/utils/feature_decorator.py +175 -0
- google/adk/version.py +2 -2
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/METADATA +10 -1
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/RECORD +92 -61
- google/adk/cli/browser/main-CS5OLUMF.js +0 -91
- google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/WHEEL +0 -0
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
|
51
|
-
self.location = location
|
53
|
+
"""Initializes the VertexAiSessionService.
|
52
54
|
|
53
|
-
|
54
|
-
|
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
|
-
|
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 =
|
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 =
|
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 =
|
237
|
-
api_client = _get_api_client(
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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 =
|
250
|
-
api_client = _get_api_client(
|
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
|
-
|
260
|
-
|
280
|
+
if app_name.isdigit():
|
281
|
+
return app_name
|
261
282
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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:
|
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]
|
google/adk/tools/__init__.py
CHANGED
@@ -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',
|
@@ -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
|
+
)
|
google/adk/tools/agent_tool.py
CHANGED
@@ -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=
|
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
|
-
|
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 =
|
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
|