google-adk 1.1.1__py3-none-any.whl → 1.2.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/agents/base_agent.py +0 -2
- google/adk/agents/invocation_context.py +3 -3
- google/adk/agents/parallel_agent.py +17 -7
- google/adk/agents/sequential_agent.py +8 -8
- google/adk/auth/auth_preprocessor.py +18 -17
- google/adk/cli/agent_graph.py +165 -23
- google/adk/cli/browser/assets/ADK-512-color.svg +9 -0
- google/adk/cli/browser/index.html +2 -2
- google/adk/cli/browser/{main-PKDNKWJE.js → main-CS5OLUMF.js} +59 -59
- google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
- google/adk/cli/cli.py +9 -9
- google/adk/cli/cli_deploy.py +157 -0
- google/adk/cli/cli_tools_click.py +228 -99
- google/adk/cli/fast_api.py +119 -34
- google/adk/cli/utils/agent_loader.py +60 -44
- google/adk/cli/utils/envs.py +1 -1
- google/adk/code_executors/unsafe_local_code_executor.py +11 -0
- google/adk/errors/__init__.py +13 -0
- google/adk/errors/not_found_error.py +28 -0
- google/adk/evaluation/agent_evaluator.py +1 -1
- google/adk/evaluation/eval_sets_manager.py +36 -6
- google/adk/evaluation/evaluation_generator.py +5 -4
- google/adk/evaluation/local_eval_sets_manager.py +101 -6
- google/adk/flows/llm_flows/agent_transfer.py +2 -2
- google/adk/flows/llm_flows/base_llm_flow.py +19 -0
- google/adk/flows/llm_flows/contents.py +4 -4
- google/adk/flows/llm_flows/functions.py +140 -127
- google/adk/memory/vertex_ai_rag_memory_service.py +2 -2
- google/adk/models/anthropic_llm.py +7 -10
- google/adk/models/google_llm.py +46 -18
- google/adk/models/lite_llm.py +63 -26
- google/adk/py.typed +0 -0
- google/adk/sessions/_session_util.py +10 -16
- google/adk/sessions/database_session_service.py +81 -66
- google/adk/sessions/vertex_ai_session_service.py +32 -6
- google/adk/telemetry.py +91 -24
- google/adk/tools/_automatic_function_calling_util.py +31 -25
- google/adk/tools/{function_parameter_parse_util.py → _function_parameter_parse_util.py} +9 -3
- google/adk/tools/_gemini_schema_util.py +158 -0
- google/adk/tools/apihub_tool/apihub_toolset.py +3 -2
- google/adk/tools/application_integration_tool/clients/connections_client.py +7 -0
- google/adk/tools/application_integration_tool/integration_connector_tool.py +5 -7
- google/adk/tools/base_tool.py +4 -8
- google/adk/tools/bigquery/bigquery_credentials.py +7 -3
- google/adk/tools/function_tool.py +4 -4
- google/adk/tools/langchain_tool.py +20 -13
- google/adk/tools/load_memory_tool.py +1 -0
- google/adk/tools/mcp_tool/conversion_utils.py +4 -2
- google/adk/tools/mcp_tool/mcp_session_manager.py +63 -5
- google/adk/tools/mcp_tool/mcp_tool.py +3 -2
- google/adk/tools/mcp_tool/mcp_toolset.py +15 -8
- google/adk/tools/openapi_tool/common/common.py +4 -43
- google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +0 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +4 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +4 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +7 -127
- google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +2 -7
- google/adk/tools/transfer_to_agent_tool.py +8 -1
- google/adk/tools/vertex_ai_search_tool.py +8 -1
- google/adk/utils/variant_utils.py +51 -0
- google/adk/version.py +1 -1
- {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/METADATA +7 -7
- {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/RECORD +66 -60
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
- {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/WHEEL +0 -0
- {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/entry_points.txt +0 -0
- {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/licenses/LICENSE +0 -0
google/adk/telemetry.py
CHANGED
@@ -21,6 +21,8 @@
|
|
21
21
|
# Agent Development Kit should be focused on the higher-level
|
22
22
|
# constructs of the framework that are not observable by the SDK.
|
23
23
|
|
24
|
+
from __future__ import annotations
|
25
|
+
|
24
26
|
import json
|
25
27
|
from typing import Any
|
26
28
|
|
@@ -31,51 +33,113 @@ from .agents.invocation_context import InvocationContext
|
|
31
33
|
from .events.event import Event
|
32
34
|
from .models.llm_request import LlmRequest
|
33
35
|
from .models.llm_response import LlmResponse
|
36
|
+
from .tools.base_tool import BaseTool
|
34
37
|
|
35
38
|
tracer = trace.get_tracer('gcp.vertex.agent')
|
36
39
|
|
37
40
|
|
41
|
+
def _safe_json_serialize(obj) -> str:
|
42
|
+
"""Convert any Python object to a JSON-serializable type or string.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
obj: The object to serialize.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
The JSON-serialized object string or <non-serializable> if the object cannot be serialized.
|
49
|
+
"""
|
50
|
+
|
51
|
+
try:
|
52
|
+
# Try direct JSON serialization first
|
53
|
+
return json.dumps(
|
54
|
+
obj, ensure_ascii=False, default=lambda o: '<not serializable>'
|
55
|
+
)
|
56
|
+
except (TypeError, OverflowError):
|
57
|
+
return '<not serializable>'
|
58
|
+
|
59
|
+
|
38
60
|
def trace_tool_call(
|
61
|
+
tool: BaseTool,
|
39
62
|
args: dict[str, Any],
|
63
|
+
function_response_event: Event,
|
40
64
|
):
|
41
65
|
"""Traces tool call.
|
42
66
|
|
43
67
|
Args:
|
68
|
+
tool: The tool that was called.
|
44
69
|
args: The arguments to the tool call.
|
70
|
+
function_response_event: The event with the function response details.
|
45
71
|
"""
|
46
72
|
span = trace.get_current_span()
|
47
73
|
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
|
48
|
-
span.set_attribute('
|
74
|
+
span.set_attribute('gen_ai.operation.name', 'execute_tool')
|
75
|
+
span.set_attribute('gen_ai.tool.name', tool.name)
|
76
|
+
span.set_attribute('gen_ai.tool.description', tool.description)
|
77
|
+
tool_call_id = '<not specified>'
|
78
|
+
tool_response = '<not specified>'
|
79
|
+
if function_response_event.content.parts:
|
80
|
+
function_response = function_response_event.content.parts[
|
81
|
+
0
|
82
|
+
].function_response
|
83
|
+
if function_response is not None:
|
84
|
+
tool_call_id = function_response.id
|
85
|
+
tool_response = function_response.response
|
49
86
|
|
87
|
+
span.set_attribute('gen_ai.tool.call.id', tool_call_id)
|
50
88
|
|
51
|
-
|
52
|
-
|
53
|
-
|
89
|
+
if not isinstance(tool_response, dict):
|
90
|
+
tool_response = {'result': tool_response}
|
91
|
+
span.set_attribute(
|
92
|
+
'gcp.vertex.agent.tool_call_args',
|
93
|
+
_safe_json_serialize(args),
|
94
|
+
)
|
95
|
+
span.set_attribute('gcp.vertex.agent.event_id', function_response_event.id)
|
96
|
+
span.set_attribute(
|
97
|
+
'gcp.vertex.agent.tool_response',
|
98
|
+
_safe_json_serialize(tool_response),
|
99
|
+
)
|
100
|
+
# Setting empty llm request and response (as UI expect these) while not
|
101
|
+
# applicable for tool_response.
|
102
|
+
span.set_attribute('gcp.vertex.agent.llm_request', '{}')
|
103
|
+
span.set_attribute(
|
104
|
+
'gcp.vertex.agent.llm_response',
|
105
|
+
'{}',
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
def trace_merged_tool_calls(
|
110
|
+
response_event_id: str,
|
54
111
|
function_response_event: Event,
|
55
112
|
):
|
56
|
-
"""Traces tool
|
113
|
+
"""Traces merged tool call events.
|
57
114
|
|
58
|
-
|
59
|
-
|
115
|
+
Calling this function is not needed for telemetry purposes. This is provided
|
116
|
+
for preventing /debug/trace requests (typically sent by web UI).
|
60
117
|
|
61
118
|
Args:
|
62
|
-
|
63
|
-
|
64
|
-
function_response_event: The function response event which can be either
|
65
|
-
merged function response for parallel function calls or individual
|
66
|
-
function response for sequential function calls.
|
119
|
+
response_event_id: The ID of the response event.
|
120
|
+
function_response_event: The merged response event.
|
67
121
|
"""
|
122
|
+
|
68
123
|
span = trace.get_current_span()
|
69
124
|
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
|
70
|
-
span.set_attribute(
|
71
|
-
|
72
|
-
)
|
73
|
-
span.set_attribute('
|
125
|
+
span.set_attribute('gen_ai.operation.name', 'execute_tool')
|
126
|
+
span.set_attribute('gen_ai.tool.name', '(merged tools)')
|
127
|
+
span.set_attribute('gen_ai.tool.description', '(merged tools)')
|
128
|
+
span.set_attribute('gen_ai.tool.call.id', response_event_id)
|
129
|
+
|
130
|
+
span.set_attribute('gcp.vertex.agent.tool_call_args', 'N/A')
|
131
|
+
span.set_attribute('gcp.vertex.agent.event_id', response_event_id)
|
132
|
+
try:
|
133
|
+
function_response_event_json = function_response_event.model_dumps_json(
|
134
|
+
exclude_none=True
|
135
|
+
)
|
136
|
+
except Exception: # pylint: disable=broad-exception-caught
|
137
|
+
function_response_event_json = '<not serializable>'
|
138
|
+
|
74
139
|
span.set_attribute(
|
75
140
|
'gcp.vertex.agent.tool_response',
|
76
|
-
|
141
|
+
function_response_event_json,
|
77
142
|
)
|
78
|
-
|
79
143
|
# Setting empty llm request and response (as UI expect these) while not
|
80
144
|
# applicable for tool_response.
|
81
145
|
span.set_attribute('gcp.vertex.agent.llm_request', '{}')
|
@@ -117,15 +181,18 @@ def trace_call_llm(
|
|
117
181
|
# Consider removing once GenAI SDK provides a way to record this info.
|
118
182
|
span.set_attribute(
|
119
183
|
'gcp.vertex.agent.llm_request',
|
120
|
-
|
121
|
-
_build_llm_request_for_trace(llm_request),
|
122
|
-
default=lambda o: '<not serializable>',
|
123
|
-
),
|
184
|
+
_safe_json_serialize(_build_llm_request_for_trace(llm_request)),
|
124
185
|
)
|
125
186
|
# Consider removing once GenAI SDK provides a way to record this info.
|
187
|
+
|
188
|
+
try:
|
189
|
+
llm_response_json = llm_response.model_dump_json(exclude_none=True)
|
190
|
+
except Exception: # pylint: disable=broad-exception-caught
|
191
|
+
llm_response_json = '<not serializable>'
|
192
|
+
|
126
193
|
span.set_attribute(
|
127
194
|
'gcp.vertex.agent.llm_response',
|
128
|
-
|
195
|
+
llm_response_json,
|
129
196
|
)
|
130
197
|
|
131
198
|
|
@@ -153,7 +220,7 @@ def trace_send_data(
|
|
153
220
|
# information still needs to be recorded by the Agent Development Kit.
|
154
221
|
span.set_attribute(
|
155
222
|
'gcp.vertex.agent.data',
|
156
|
-
|
223
|
+
_safe_json_serialize([
|
157
224
|
types.Content(role=content.role, parts=content.parts).model_dump(
|
158
225
|
exclude_none=True
|
159
226
|
)
|
@@ -12,10 +12,11 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
|
15
|
+
from __future__ import annotations
|
16
16
|
|
17
17
|
import inspect
|
18
18
|
from types import FunctionType
|
19
|
+
import typing
|
19
20
|
from typing import Any
|
20
21
|
from typing import Callable
|
21
22
|
from typing import Dict
|
@@ -29,7 +30,8 @@ from pydantic import BaseModel
|
|
29
30
|
from pydantic import create_model
|
30
31
|
from pydantic import fields as pydantic_fields
|
31
32
|
|
32
|
-
from . import
|
33
|
+
from . import _function_parameter_parse_util
|
34
|
+
from ..utils.variant_utils import GoogleLLMVariant
|
33
35
|
|
34
36
|
_py_type_2_schema_type = {
|
35
37
|
'str': types.Type.STRING,
|
@@ -193,7 +195,7 @@ def _get_return_type(func: Callable) -> Any:
|
|
193
195
|
def build_function_declaration(
|
194
196
|
func: Union[Callable, BaseModel],
|
195
197
|
ignore_params: Optional[list[str]] = None,
|
196
|
-
variant:
|
198
|
+
variant: GoogleLLMVariant = GoogleLLMVariant.GEMINI_API,
|
197
199
|
) -> types.FunctionDeclaration:
|
198
200
|
signature = inspect.signature(func)
|
199
201
|
should_update_signature = False
|
@@ -227,6 +229,7 @@ def build_function_declaration(
|
|
227
229
|
func.__closure__,
|
228
230
|
)
|
229
231
|
new_func.__signature__ = new_sig
|
232
|
+
new_func.__doc__ = func.__doc__
|
230
233
|
|
231
234
|
return (
|
232
235
|
from_function_with_options(func, variant)
|
@@ -289,16 +292,9 @@ def build_function_declaration_util(
|
|
289
292
|
|
290
293
|
def from_function_with_options(
|
291
294
|
func: Callable,
|
292
|
-
variant:
|
295
|
+
variant: GoogleLLMVariant = GoogleLLMVariant.GEMINI_API,
|
293
296
|
) -> 'types.FunctionDeclaration':
|
294
297
|
|
295
|
-
supported_variants = ['GOOGLE_AI', 'VERTEX_AI', 'DEFAULT']
|
296
|
-
if variant not in supported_variants:
|
297
|
-
raise ValueError(
|
298
|
-
f'Unsupported variant: {variant}. Supported variants are:'
|
299
|
-
f' {", ".join(supported_variants)}'
|
300
|
-
)
|
301
|
-
|
302
298
|
parameters_properties = {}
|
303
299
|
for name, param in inspect.signature(func).parameters.items():
|
304
300
|
if param.kind in (
|
@@ -306,7 +302,11 @@ def from_function_with_options(
|
|
306
302
|
inspect.Parameter.KEYWORD_ONLY,
|
307
303
|
inspect.Parameter.POSITIONAL_ONLY,
|
308
304
|
):
|
309
|
-
|
305
|
+
# This snippet catches the case when type hints are stored as strings
|
306
|
+
if isinstance(param.annotation, str):
|
307
|
+
param = param.replace(annotation=typing.get_type_hints(func)[name])
|
308
|
+
|
309
|
+
schema = _function_parameter_parse_util._parse_schema_from_parameter(
|
310
310
|
variant, param, func.__name__
|
311
311
|
)
|
312
312
|
parameters_properties[name] = schema
|
@@ -319,27 +319,33 @@ def from_function_with_options(
|
|
319
319
|
type='OBJECT',
|
320
320
|
properties=parameters_properties,
|
321
321
|
)
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
if not variant == 'VERTEX_AI':
|
322
|
+
declaration.parameters.required = (
|
323
|
+
_function_parameter_parse_util._get_required_fields(
|
324
|
+
declaration.parameters
|
325
|
+
)
|
326
|
+
)
|
327
|
+
if variant == GoogleLLMVariant.GEMINI_API:
|
329
328
|
return declaration
|
330
329
|
|
331
330
|
return_annotation = inspect.signature(func).return_annotation
|
332
331
|
if return_annotation is inspect._empty:
|
333
332
|
return declaration
|
334
333
|
|
334
|
+
return_value = inspect.Parameter(
|
335
|
+
'return_value',
|
336
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
337
|
+
annotation=return_annotation,
|
338
|
+
)
|
339
|
+
# This snippet catches the case when type hints are stored as strings
|
340
|
+
if isinstance(return_value.annotation, str):
|
341
|
+
return_value = return_value.replace(
|
342
|
+
annotation=typing.get_type_hints(func)['return']
|
343
|
+
)
|
344
|
+
|
335
345
|
declaration.response = (
|
336
|
-
|
346
|
+
_function_parameter_parse_util._parse_schema_from_parameter(
|
337
347
|
variant,
|
338
|
-
|
339
|
-
'return_value',
|
340
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
341
|
-
annotation=return_annotation,
|
342
|
-
),
|
348
|
+
return_value,
|
343
349
|
func.__name__,
|
344
350
|
)
|
345
351
|
)
|
@@ -13,6 +13,8 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
#
|
15
15
|
|
16
|
+
from __future__ import annotations
|
17
|
+
|
16
18
|
import inspect
|
17
19
|
import logging
|
18
20
|
import types as typing_types
|
@@ -26,6 +28,8 @@ from typing import Union
|
|
26
28
|
from google.genai import types
|
27
29
|
import pydantic
|
28
30
|
|
31
|
+
from ..utils.variant_utils import GoogleLLMVariant
|
32
|
+
|
29
33
|
_py_builtin_type_to_schema_type = {
|
30
34
|
str: types.Type.STRING,
|
31
35
|
int: types.Type.INTEGER,
|
@@ -61,8 +65,10 @@ def _update_for_default_if_mldev(schema: types.Schema):
|
|
61
65
|
)
|
62
66
|
|
63
67
|
|
64
|
-
def _raise_if_schema_unsupported(
|
65
|
-
|
68
|
+
def _raise_if_schema_unsupported(
|
69
|
+
variant: GoogleLLMVariant, schema: types.Schema
|
70
|
+
):
|
71
|
+
if variant == GoogleLLMVariant.GEMINI_API:
|
66
72
|
_raise_for_any_of_if_mldev(schema)
|
67
73
|
_update_for_default_if_mldev(schema)
|
68
74
|
|
@@ -114,7 +120,7 @@ def _is_default_value_compatible(
|
|
114
120
|
|
115
121
|
|
116
122
|
def _parse_schema_from_parameter(
|
117
|
-
variant:
|
123
|
+
variant: GoogleLLMVariant, param: inspect.Parameter, func_name: str
|
118
124
|
) -> types.Schema:
|
119
125
|
"""parse schema from parameter.
|
120
126
|
|
@@ -0,0 +1,158 @@
|
|
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 re
|
18
|
+
from typing import Any
|
19
|
+
from typing import Optional
|
20
|
+
|
21
|
+
from google.genai.types import JSONSchema
|
22
|
+
from google.genai.types import Schema
|
23
|
+
from pydantic import Field
|
24
|
+
|
25
|
+
from ..utils.variant_utils import get_google_llm_variant
|
26
|
+
|
27
|
+
|
28
|
+
class _ExtendedJSONSchema(JSONSchema):
|
29
|
+
property_ordering: Optional[list[str]] = Field(
|
30
|
+
default=None,
|
31
|
+
description="""Optional. The order of the properties. Not a standard field in open api spec. Only used to support the order of the properties.""",
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def _to_snake_case(text: str) -> str:
|
36
|
+
"""Converts a string into snake_case.
|
37
|
+
|
38
|
+
Handles lowerCamelCase, UpperCamelCase, or space-separated case, acronyms
|
39
|
+
(e.g., "REST API") and consecutive uppercase letters correctly. Also handles
|
40
|
+
mixed cases with and without spaces.
|
41
|
+
|
42
|
+
Examples:
|
43
|
+
```
|
44
|
+
to_snake_case('camelCase') -> 'camel_case'
|
45
|
+
to_snake_case('UpperCamelCase') -> 'upper_camel_case'
|
46
|
+
to_snake_case('space separated') -> 'space_separated'
|
47
|
+
```
|
48
|
+
|
49
|
+
Args:
|
50
|
+
text: The input string.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
The snake_case version of the string.
|
54
|
+
"""
|
55
|
+
|
56
|
+
# Handle spaces and non-alphanumeric characters (replace with underscores)
|
57
|
+
text = re.sub(r"[^a-zA-Z0-9]+", "_", text)
|
58
|
+
|
59
|
+
# Insert underscores before uppercase letters (handling both CamelCases)
|
60
|
+
text = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", text) # lowerCamelCase
|
61
|
+
text = re.sub(
|
62
|
+
r"([A-Z]+)([A-Z][a-z])", r"\1_\2", text
|
63
|
+
) # UpperCamelCase and acronyms
|
64
|
+
|
65
|
+
# Convert to lowercase
|
66
|
+
text = text.lower()
|
67
|
+
|
68
|
+
# Remove consecutive underscores (clean up extra underscores)
|
69
|
+
text = re.sub(r"_+", "_", text)
|
70
|
+
|
71
|
+
# Remove leading and trailing underscores
|
72
|
+
text = text.strip("_")
|
73
|
+
|
74
|
+
return text
|
75
|
+
|
76
|
+
|
77
|
+
def _sanitize_schema_type(schema: dict[str, Any]) -> dict[str, Any]:
|
78
|
+
if ("type" not in schema or not schema["type"]) and schema.keys().isdisjoint(
|
79
|
+
schema
|
80
|
+
):
|
81
|
+
schema["type"] = "object"
|
82
|
+
if isinstance(schema.get("type"), list):
|
83
|
+
nullable = False
|
84
|
+
non_null_type = None
|
85
|
+
for t in schema["type"]:
|
86
|
+
if t == "null":
|
87
|
+
nullable = True
|
88
|
+
elif not non_null_type:
|
89
|
+
non_null_type = t
|
90
|
+
if not non_null_type:
|
91
|
+
non_null_type = "object"
|
92
|
+
if nullable:
|
93
|
+
schema["type"] = [non_null_type, "null"]
|
94
|
+
else:
|
95
|
+
schema["type"] = non_null_type
|
96
|
+
elif schema.get("type") == "null":
|
97
|
+
schema["type"] = ["object", "null"]
|
98
|
+
|
99
|
+
return schema
|
100
|
+
|
101
|
+
|
102
|
+
def _sanitize_schema_formats_for_gemini(
|
103
|
+
schema: dict[str, Any],
|
104
|
+
) -> dict[str, Any]:
|
105
|
+
"""Filters the schema to only include fields that are supported by JSONSchema."""
|
106
|
+
supported_fields: set[str] = set(_ExtendedJSONSchema.model_fields.keys())
|
107
|
+
schema_field_names: set[str] = {"items"} # 'additional_properties' to come
|
108
|
+
list_schema_field_names: set[str] = {
|
109
|
+
"any_of", # 'one_of', 'all_of', 'not' to come
|
110
|
+
}
|
111
|
+
snake_case_schema = {}
|
112
|
+
dict_schema_field_names: tuple[str] = ("properties",) # 'defs' to come
|
113
|
+
for field_name, field_value in schema.items():
|
114
|
+
field_name = _to_snake_case(field_name)
|
115
|
+
if field_name in schema_field_names:
|
116
|
+
snake_case_schema[field_name] = _sanitize_schema_formats_for_gemini(
|
117
|
+
field_value
|
118
|
+
)
|
119
|
+
elif field_name in list_schema_field_names:
|
120
|
+
snake_case_schema[field_name] = [
|
121
|
+
_sanitize_schema_formats_for_gemini(value) for value in field_value
|
122
|
+
]
|
123
|
+
elif field_name in dict_schema_field_names:
|
124
|
+
snake_case_schema[field_name] = {
|
125
|
+
key: _sanitize_schema_formats_for_gemini(value)
|
126
|
+
for key, value in field_value.items()
|
127
|
+
}
|
128
|
+
# special handle of format field
|
129
|
+
elif field_name == "format" and field_value:
|
130
|
+
current_type = schema.get("type")
|
131
|
+
if (
|
132
|
+
# only "int32" and "int64" are supported for integer or number type
|
133
|
+
(current_type == "integer" or current_type == "number")
|
134
|
+
and field_value in ("int32", "int64")
|
135
|
+
or
|
136
|
+
# only 'enum' and 'date-time' are supported for STRING type"
|
137
|
+
(current_type == "string" and field_value in ("date-time", "enum"))
|
138
|
+
):
|
139
|
+
snake_case_schema[field_name] = field_value
|
140
|
+
elif field_name in supported_fields and field_value is not None:
|
141
|
+
snake_case_schema[field_name] = field_value
|
142
|
+
|
143
|
+
return _sanitize_schema_type(snake_case_schema)
|
144
|
+
|
145
|
+
|
146
|
+
def _to_gemini_schema(openapi_schema: dict[str, Any]) -> Schema:
|
147
|
+
"""Converts an OpenAPI schema dictionary to a Gemini Schema object."""
|
148
|
+
if openapi_schema is None:
|
149
|
+
return None
|
150
|
+
|
151
|
+
if not isinstance(openapi_schema, dict):
|
152
|
+
raise TypeError("openapi_schema must be a dictionary")
|
153
|
+
|
154
|
+
openapi_schema = _sanitize_schema_formats_for_gemini(openapi_schema)
|
155
|
+
return Schema.from_json_schema(
|
156
|
+
json_schema=_ExtendedJSONSchema.model_validate(openapi_schema),
|
157
|
+
api_option=get_google_llm_variant(),
|
158
|
+
)
|
@@ -12,6 +12,7 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
from __future__ import annotations
|
15
16
|
|
16
17
|
from typing import List
|
17
18
|
from typing import Optional
|
@@ -23,9 +24,9 @@ import yaml
|
|
23
24
|
from ...agents.readonly_context import ReadonlyContext
|
24
25
|
from ...auth.auth_credential import AuthCredential
|
25
26
|
from ...auth.auth_schemes import AuthScheme
|
27
|
+
from .._gemini_schema_util import _to_snake_case
|
26
28
|
from ..base_toolset import BaseToolset
|
27
29
|
from ..base_toolset import ToolPredicate
|
28
|
-
from ..openapi_tool.common.common import to_snake_case
|
29
30
|
from ..openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
|
30
31
|
from ..openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
|
31
32
|
from .clients.apihub_client import APIHubClient
|
@@ -171,7 +172,7 @@ class APIHubToolset(BaseToolset):
|
|
171
172
|
if not spec_dict:
|
172
173
|
return
|
173
174
|
|
174
|
-
self.name = self.name or
|
175
|
+
self.name = self.name or _to_snake_case(
|
175
176
|
spec_dict.get('info', {}).get('title', 'unnamed')
|
176
177
|
)
|
177
178
|
self.description = self.description or spec_dict.get('info', {}).get(
|
@@ -252,6 +252,12 @@ class ConnectionsClient:
|
|
252
252
|
"Timeout in seconds for execution of custom query"
|
253
253
|
),
|
254
254
|
},
|
255
|
+
"sortByColumns": {
|
256
|
+
"type": "array",
|
257
|
+
"items": {"type": "string"},
|
258
|
+
"default": [],
|
259
|
+
"description": "Column to sort the results by",
|
260
|
+
},
|
255
261
|
"connectorOutputPayload": {"type": "object"},
|
256
262
|
"nextPageToken": {"type": "string"},
|
257
263
|
"execute-connector_Response": {
|
@@ -665,6 +671,7 @@ class ConnectionsClient:
|
|
665
671
|
"serviceName": {"$ref": "#/components/schemas/serviceName"},
|
666
672
|
"host": {"$ref": "#/components/schemas/host"},
|
667
673
|
"entity": {"$ref": "#/components/schemas/entity"},
|
674
|
+
"sortByColumns": {"$ref": "#/components/schemas/sortByColumns"},
|
668
675
|
"dynamicAuthConfig": {
|
669
676
|
"$ref": "#/components/schemas/dynamicAuthConfig"
|
670
677
|
},
|
@@ -12,6 +12,8 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
from __future__ import annotations
|
16
|
+
|
15
17
|
import logging
|
16
18
|
from typing import Any
|
17
19
|
from typing import Dict
|
@@ -24,8 +26,8 @@ from typing_extensions import override
|
|
24
26
|
from .. import BaseTool
|
25
27
|
from ...auth.auth_credential import AuthCredential
|
26
28
|
from ...auth.auth_schemes import AuthScheme
|
29
|
+
from .._gemini_schema_util import _to_gemini_schema
|
27
30
|
from ..openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
|
28
|
-
from ..openapi_tool.openapi_spec_parser.rest_api_tool import to_gemini_schema
|
29
31
|
from ..openapi_tool.openapi_spec_parser.tool_auth_handler import ToolAuthHandler
|
30
32
|
from ..tool_context import ToolContext
|
31
33
|
|
@@ -62,11 +64,7 @@ class IntegrationConnectorTool(BaseTool):
|
|
62
64
|
'dynamic_auth_config',
|
63
65
|
]
|
64
66
|
|
65
|
-
OPTIONAL_FIELDS = [
|
66
|
-
'page_size',
|
67
|
-
'page_token',
|
68
|
-
'filter',
|
69
|
-
]
|
67
|
+
OPTIONAL_FIELDS = ['page_size', 'page_token', 'filter', 'sortByColumns']
|
70
68
|
|
71
69
|
def __init__(
|
72
70
|
self,
|
@@ -128,7 +126,7 @@ class IntegrationConnectorTool(BaseTool):
|
|
128
126
|
if field in schema_dict['required']:
|
129
127
|
schema_dict['required'].remove(field)
|
130
128
|
|
131
|
-
parameters =
|
129
|
+
parameters = _to_gemini_schema(schema_dict)
|
132
130
|
function_decl = FunctionDeclaration(
|
133
131
|
name=self.name, description=self.description, parameters=parameters
|
134
132
|
)
|
google/adk/tools/base_tool.py
CHANGED
@@ -15,14 +15,14 @@
|
|
15
15
|
from __future__ import annotations
|
16
16
|
|
17
17
|
from abc import ABC
|
18
|
-
import os
|
19
18
|
from typing import Any
|
20
19
|
from typing import Optional
|
21
20
|
from typing import TYPE_CHECKING
|
22
21
|
|
23
|
-
from deprecated import deprecated
|
24
22
|
from google.genai import types
|
25
23
|
|
24
|
+
from ..utils.variant_utils import get_google_llm_variant
|
25
|
+
from ..utils.variant_utils import GoogleLLMVariant
|
26
26
|
from .tool_context import ToolContext
|
27
27
|
|
28
28
|
if TYPE_CHECKING:
|
@@ -119,12 +119,8 @@ class BaseTool(ABC):
|
|
119
119
|
)
|
120
120
|
|
121
121
|
@property
|
122
|
-
def _api_variant(self) ->
|
123
|
-
|
124
|
-
'true',
|
125
|
-
'1',
|
126
|
-
]
|
127
|
-
return 'VERTEX_AI' if use_vertexai else 'GOOGLE_AI'
|
122
|
+
def _api_variant(self) -> GoogleLLMVariant:
|
123
|
+
return get_google_llm_variant()
|
128
124
|
|
129
125
|
|
130
126
|
def _find_tool_with_function_declarations(
|
@@ -34,6 +34,7 @@ from ...auth import OAuth2Auth
|
|
34
34
|
from ..tool_context import ToolContext
|
35
35
|
|
36
36
|
BIGQUERY_TOKEN_CACHE_KEY = "bigquery_token_cache"
|
37
|
+
BIGQUERY_DEFAULT_SCOPE = ["https://www.googleapis.com/auth/bigquery"]
|
37
38
|
|
38
39
|
|
39
40
|
class BigQueryCredentialsConfig(BaseModel):
|
@@ -66,15 +67,14 @@ class BigQueryCredentialsConfig(BaseModel):
|
|
66
67
|
client_secret: Optional[str] = None
|
67
68
|
"""the oauth client secret to use."""
|
68
69
|
scopes: Optional[List[str]] = None
|
69
|
-
"""the scopes to use.
|
70
|
-
"""
|
70
|
+
"""the scopes to use."""
|
71
71
|
|
72
72
|
@model_validator(mode="after")
|
73
73
|
def __post_init__(self) -> BigQueryCredentialsConfig:
|
74
74
|
"""Validate that either credentials or client ID/secret are provided."""
|
75
75
|
if not self.credentials and (not self.client_id or not self.client_secret):
|
76
76
|
raise ValueError(
|
77
|
-
"Must provide either credentials or client_id
|
77
|
+
"Must provide either credentials or client_id and client_secret pair."
|
78
78
|
)
|
79
79
|
if self.credentials and (
|
80
80
|
self.client_id or self.client_secret or self.scopes
|
@@ -88,6 +88,10 @@ class BigQueryCredentialsConfig(BaseModel):
|
|
88
88
|
self.client_id = self.credentials.client_id
|
89
89
|
self.client_secret = self.credentials.client_secret
|
90
90
|
self.scopes = self.credentials.scopes
|
91
|
+
|
92
|
+
if not self.scopes:
|
93
|
+
self.scopes = BIGQUERY_DEFAULT_SCOPE
|
94
|
+
|
91
95
|
return self
|
92
96
|
|
93
97
|
|
@@ -46,14 +46,14 @@ class FunctionTool(BaseTool):
|
|
46
46
|
|
47
47
|
# Get documentation (prioritize direct __doc__ if available)
|
48
48
|
if hasattr(func, '__doc__') and func.__doc__:
|
49
|
-
doc = func.__doc__
|
49
|
+
doc = inspect.cleandoc(func.__doc__)
|
50
50
|
elif (
|
51
51
|
hasattr(func, '__call__')
|
52
52
|
and hasattr(func.__call__, '__doc__')
|
53
53
|
and func.__call__.__doc__
|
54
54
|
):
|
55
55
|
# For callable objects, try to get docstring from __call__ method
|
56
|
-
doc = func.__call__.__doc__
|
56
|
+
doc = inspect.cleandoc(func.__call__.__doc__)
|
57
57
|
|
58
58
|
super().__init__(name=name, description=doc)
|
59
59
|
self.func = func
|
@@ -107,9 +107,9 @@ You could retry calling this tool, but it is IMPORTANT for you to provide all th
|
|
107
107
|
or hasattr(self.func, '__call__')
|
108
108
|
and inspect.iscoroutinefunction(self.func.__call__)
|
109
109
|
):
|
110
|
-
return await self.func(**args_to_call)
|
110
|
+
return await self.func(**args_to_call)
|
111
111
|
else:
|
112
|
-
return self.func(**args_to_call)
|
112
|
+
return self.func(**args_to_call)
|
113
113
|
|
114
114
|
# TODO(hangfei): fix call live for function stream.
|
115
115
|
async def _call_live(
|