From 6bbbf979e7a234893b2497c6bc5786b8bbaabb69 Mon Sep 17 00:00:00 2001 From: Gautier Masse Date: Tue, 21 Apr 2026 10:00:41 +0200 Subject: [PATCH] fix(tools): handle tuple schemas in function parsing fallback --- .../tools/_function_parameter_parse_util.py | 40 +++++++++++++++++++ .../tools/test_from_function_with_options.py | 38 ++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/google/adk/tools/_function_parameter_parse_util.py b/src/google/adk/tools/_function_parameter_parse_util.py index a8e98980d5..ca2308f65f 100644 --- a/src/google/adk/tools/_function_parameter_parse_util.py +++ b/src/google/adk/tools/_function_parameter_parse_util.py @@ -93,6 +93,45 @@ def _add_unevaluated_items_to_fixed_len_tuple_schema( return json_schema +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 _raise_for_unsupported_param( param: inspect.Parameter, func_name: str, @@ -131,6 +170,7 @@ def _generate_json_schema_for_parameter( json_schema_dict = _add_unevaluated_items_to_fixed_len_tuple_schema( json_schema_dict ) + json_schema_dict = _normalize_tuple_schema_for_genai_schema(json_schema_dict) return json_schema_dict diff --git a/tests/unittests/tools/test_from_function_with_options.py b/tests/unittests/tools/test_from_function_with_options.py index 537094da39..6665d0a8ca 100644 --- a/tests/unittests/tools/test_from_function_with_options.py +++ b/tests/unittests/tools/test_from_function_with_options.py @@ -361,3 +361,41 @@ def complex_tool( ), }, ) + + +def test_tuple_types_work_in_json_schema_fallback(): + """Test that tuple schemas work in json schema fallback.""" + + def generate_image( + prompt: str, + input_bytes: list[tuple[bytes, str]] | None = None, + ) -> dict[str, str]: + """Generate an image from a prompt.""" + del input_bytes + return {'status': prompt} + + declaration = _automatic_function_calling_util.from_function_with_options( + generate_image, GoogleLLMVariant.GEMINI_API + ) + + assert declaration.parameters is not None + assert declaration.parameters.required == ['prompt'] + input_bytes_schema = declaration.parameters.properties['input_bytes'] + assert input_bytes_schema.nullable is True + assert input_bytes_schema.any_of is not None + + array_schema = next( + schema + for schema in input_bytes_schema.any_of + if schema.type == types.Type.ARRAY + ) + assert array_schema.items is not None + assert array_schema.items.type == types.Type.ARRAY + assert array_schema.items.max_items == 2 + assert array_schema.items.min_items == 2 + assert array_schema.items.items is not None + assert array_schema.items.items.any_of is not None + assert len(array_schema.items.items.any_of) == 2 + assert array_schema.items.items.any_of[0].type == types.Type.STRING + assert array_schema.items.items.any_of[0].format == 'binary' + assert array_schema.items.items.any_of[1].type == types.Type.STRING