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.
Files changed (67) hide show
  1. google/adk/agents/base_agent.py +0 -2
  2. google/adk/agents/invocation_context.py +3 -3
  3. google/adk/agents/parallel_agent.py +17 -7
  4. google/adk/agents/sequential_agent.py +8 -8
  5. google/adk/auth/auth_preprocessor.py +18 -17
  6. google/adk/cli/agent_graph.py +165 -23
  7. google/adk/cli/browser/assets/ADK-512-color.svg +9 -0
  8. google/adk/cli/browser/index.html +2 -2
  9. google/adk/cli/browser/{main-PKDNKWJE.js → main-CS5OLUMF.js} +59 -59
  10. google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
  11. google/adk/cli/cli.py +9 -9
  12. google/adk/cli/cli_deploy.py +157 -0
  13. google/adk/cli/cli_tools_click.py +228 -99
  14. google/adk/cli/fast_api.py +119 -34
  15. google/adk/cli/utils/agent_loader.py +60 -44
  16. google/adk/cli/utils/envs.py +1 -1
  17. google/adk/code_executors/unsafe_local_code_executor.py +11 -0
  18. google/adk/errors/__init__.py +13 -0
  19. google/adk/errors/not_found_error.py +28 -0
  20. google/adk/evaluation/agent_evaluator.py +1 -1
  21. google/adk/evaluation/eval_sets_manager.py +36 -6
  22. google/adk/evaluation/evaluation_generator.py +5 -4
  23. google/adk/evaluation/local_eval_sets_manager.py +101 -6
  24. google/adk/flows/llm_flows/agent_transfer.py +2 -2
  25. google/adk/flows/llm_flows/base_llm_flow.py +19 -0
  26. google/adk/flows/llm_flows/contents.py +4 -4
  27. google/adk/flows/llm_flows/functions.py +140 -127
  28. google/adk/memory/vertex_ai_rag_memory_service.py +2 -2
  29. google/adk/models/anthropic_llm.py +7 -10
  30. google/adk/models/google_llm.py +46 -18
  31. google/adk/models/lite_llm.py +63 -26
  32. google/adk/py.typed +0 -0
  33. google/adk/sessions/_session_util.py +10 -16
  34. google/adk/sessions/database_session_service.py +81 -66
  35. google/adk/sessions/vertex_ai_session_service.py +32 -6
  36. google/adk/telemetry.py +91 -24
  37. google/adk/tools/_automatic_function_calling_util.py +31 -25
  38. google/adk/tools/{function_parameter_parse_util.py → _function_parameter_parse_util.py} +9 -3
  39. google/adk/tools/_gemini_schema_util.py +158 -0
  40. google/adk/tools/apihub_tool/apihub_toolset.py +3 -2
  41. google/adk/tools/application_integration_tool/clients/connections_client.py +7 -0
  42. google/adk/tools/application_integration_tool/integration_connector_tool.py +5 -7
  43. google/adk/tools/base_tool.py +4 -8
  44. google/adk/tools/bigquery/bigquery_credentials.py +7 -3
  45. google/adk/tools/function_tool.py +4 -4
  46. google/adk/tools/langchain_tool.py +20 -13
  47. google/adk/tools/load_memory_tool.py +1 -0
  48. google/adk/tools/mcp_tool/conversion_utils.py +4 -2
  49. google/adk/tools/mcp_tool/mcp_session_manager.py +63 -5
  50. google/adk/tools/mcp_tool/mcp_tool.py +3 -2
  51. google/adk/tools/mcp_tool/mcp_toolset.py +15 -8
  52. google/adk/tools/openapi_tool/common/common.py +4 -43
  53. google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +0 -2
  54. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +4 -2
  55. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +4 -2
  56. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +7 -127
  57. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +2 -7
  58. google/adk/tools/transfer_to_agent_tool.py +8 -1
  59. google/adk/tools/vertex_ai_search_tool.py +8 -1
  60. google/adk/utils/variant_utils.py +51 -0
  61. google/adk/version.py +1 -1
  62. {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/METADATA +7 -7
  63. {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/RECORD +66 -60
  64. google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
  65. {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/WHEEL +0 -0
  66. {google_adk-1.1.1.dist-info → google_adk-1.2.0.dist-info}/entry_points.txt +0 -0
  67. {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('gcp.vertex.agent.tool_call_args', json.dumps(args))
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
- def trace_tool_response(
52
- invocation_context: InvocationContext,
53
- event_id: str,
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 response event.
113
+ """Traces merged tool call events.
57
114
 
58
- This function records details about the tool response event as attributes on
59
- the current OpenTelemetry span.
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
- invocation_context: The invocation context for the current agent run.
63
- event_id: The ID of the event.
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
- 'gcp.vertex.agent.invocation_id', invocation_context.invocation_id
72
- )
73
- span.set_attribute('gcp.vertex.agent.event_id', event_id)
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
- function_response_event.model_dump_json(exclude_none=True),
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
- json.dumps(
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
- llm_response.model_dump_json(exclude_none=True),
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
- json.dumps([
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
- """Forked from google3/third_party/py/google/genai/_automatic_function_calling_util.py temporarily."""
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 function_parameter_parse_util
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: Literal['GOOGLE_AI', 'VERTEX_AI', 'DEFAULT'] = 'GOOGLE_AI',
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: Literal['GOOGLE_AI', 'VERTEX_AI', 'DEFAULT'] = 'GOOGLE_AI',
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
- schema = function_parameter_parse_util._parse_schema_from_parameter(
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
- if variant == 'VERTEX_AI':
323
- declaration.parameters.required = (
324
- function_parameter_parse_util._get_required_fields(
325
- declaration.parameters
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
- function_parameter_parse_util._parse_schema_from_parameter(
346
+ _function_parameter_parse_util._parse_schema_from_parameter(
337
347
  variant,
338
- inspect.Parameter(
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(variant: str, schema: types.Schema):
65
- if not variant == 'VERTEX_AI':
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: str, param: inspect.Parameter, func_name: str
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 to_snake_case(
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 = to_gemini_schema(schema_dict)
129
+ parameters = _to_gemini_schema(schema_dict)
132
130
  function_decl = FunctionDeclaration(
133
131
  name=self.name, description=self.description, parameters=parameters
134
132
  )
@@ -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) -> str:
123
- use_vertexai = os.environ.get('GOOGLE_GENAI_USE_VERTEXAI', '0').lower() in [
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 abd client_secret pair."
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) or {}
110
+ return await self.func(**args_to_call)
111
111
  else:
112
- return self.func(**args_to_call) or {}
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(