diff --git a/src/google/adk/tools/_function_parameter_parse_util.py b/src/google/adk/tools/_function_parameter_parse_util.py index a8e98980d5..17621bcb68 100644 --- a/src/google/adk/tools/_function_parameter_parse_util.py +++ b/src/google/adk/tools/_function_parameter_parse_util.py @@ -21,6 +21,7 @@ import types as typing_types from typing import _GenericAlias from typing import Any +from typing import cast from typing import get_args from typing import get_origin from typing import Literal @@ -118,6 +119,44 @@ def _raise_for_invalid_enum_value(param: inspect.Parameter): ) +def _normalize_tuple_schema_for_genai_schema( + json_schema: Any, +) -> Any: + """Normalizes tuple schema keywords unsupported by `types.Schema`. + + Pydantic emits `prefixItems` for fixed-length tuples. `types.Schema` does not + support `prefixItems`, so we convert tuple item definitions into + `items.anyOf`. We also drop `unevaluatedItems`, which is unsupported by + `types.Schema`. + """ + if isinstance(json_schema, list): + return [ + _normalize_tuple_schema_for_genai_schema(item) for item in json_schema + ] + if not isinstance(json_schema, dict): + return json_schema + + normalized_schema = { + key: _normalize_tuple_schema_for_genai_schema(value) + for key, value in json_schema.items() + if key != 'unevaluatedItems' + } + + prefix_items = normalized_schema.pop('prefixItems', None) + if isinstance(prefix_items, list): + if len(prefix_items) == 1: + normalized_schema['items'] = prefix_items[0] + elif prefix_items: + normalized_schema['items'] = {'anyOf': prefix_items} + + # Pydantic can emit `items: false` for tuple schemas, which is unsupported by + # `types.Schema`. + if normalized_schema.get('items') is False: + normalized_schema.pop('items') + + return normalized_schema + + def _generate_json_schema_for_parameter( param: inspect.Parameter, ) -> dict[str, Any]: @@ -131,7 +170,10 @@ def _generate_json_schema_for_parameter( json_schema_dict = _add_unevaluated_items_to_fixed_len_tuple_schema( json_schema_dict ) - return json_schema_dict + return cast( + dict[str, Any], + _normalize_tuple_schema_for_genai_schema(json_schema_dict), + ) def _is_builtin_primitive_or_compound( diff --git a/tests/unittests/tools/test_function_parameter_parse_util.py b/tests/unittests/tools/test_function_parameter_parse_util.py new file mode 100644 index 0000000000..fc441d41df --- /dev/null +++ b/tests/unittests/tools/test_function_parameter_parse_util.py @@ -0,0 +1,94 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +from typing import Any + +from google.adk.tools import _function_parameter_parse_util + + +def test_normalize_strips_prefixItems() -> None: + schema: dict[str, Any] = { + "type": "array", + "prefixItems": [{"type": "string"}, {"type": "number"}], + "minItems": 2, + "maxItems": 2, + "unevaluatedItems": False, + } + normalized = ( + _function_parameter_parse_util._normalize_tuple_schema_for_genai_schema( + schema + ) + ) + assert "prefixItems" not in normalized + assert "unevaluatedItems" not in normalized + assert normalized["items"] == { + "anyOf": [{"type": "string"}, {"type": "number"}] + } + + +def test_normalize_strips_unevaluatedItems() -> None: + schema: dict[str, Any] = { + "type": "object", + "properties": { + "field1": {"type": "string"}, + }, + "unevaluatedItems": False, + } + normalized = ( + _function_parameter_parse_util._normalize_tuple_schema_for_genai_schema( + schema + ) + ) + assert "unevaluatedItems" not in normalized + assert normalized["properties"] == {"field1": {"type": "string"}} + + +def test_normalize_handles_items_false() -> None: + schema: dict[str, Any] = { + "type": "array", + "prefixItems": [{"type": "string"}], + "items": False, + } + normalized = ( + _function_parameter_parse_util._normalize_tuple_schema_for_genai_schema( + schema + ) + ) + assert "items" in normalized + assert normalized["items"] == {"type": "string"} + assert normalized.get("items") is not False + + +def test_normalize_handles_nested_schemas() -> None: + schema: dict[str, Any] = { + "type": "object", + "properties": { + "field1": { + "type": "array", + "prefixItems": [{"type": "string"}], + "unevaluatedItems": False, + } + }, + } + normalized = ( + _function_parameter_parse_util._normalize_tuple_schema_for_genai_schema( + schema + ) + ) + assert "unevaluatedItems" not in normalized["properties"]["field1"] + assert "prefixItems" not in normalized["properties"]["field1"] + assert normalized["properties"]["field1"]["items"] == {"type": "string"}