From f5695e288272eb4a4c0537c329c538a9e4d76304 Mon Sep 17 00:00:00 2001 From: weiguangli-io Date: Sun, 29 Mar 2026 21:22:58 +0800 Subject: [PATCH] fix: emit additional_properties in dict branch of _parse_schema_from_parameter The dict origin branch in _parse_schema_from_parameter previously returned Schema(type=OBJECT) without any value type information for parameterized dict types like dict[str, str]. This caused Gemini to receive a bare {"type": "object"} with no constraints, leading to empty tool arguments and infinite retry loops when combined with output_schema. Now the dict branch parses the value type argument and sets additional_properties with the corresponding schema, so dict[str, V] correctly emits {"type": "object", "additionalProperties": }. Fixes #4868 --- .../tools/_function_parameter_parse_util.py | 11 ++++ .../tools/test_build_function_declaration.py | 53 +++++++++++++++++++ 2 files changed, 64 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..6da14619e7 100644 --- a/src/google/adk/tools/_function_parameter_parse_util.py +++ b/src/google/adk/tools/_function_parameter_parse_util.py @@ -294,6 +294,17 @@ def _parse_schema_from_parameter( args = get_args(param.annotation) if origin is dict: schema.type = types.Type.OBJECT + if len(args) == 2: + value_type = args[1] + schema.additional_properties = _parse_schema_from_parameter( + variant, + inspect.Parameter( + 'value', + inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=value_type, + ), + func_name, + ) if param.default is not inspect.Parameter.empty: if not _is_default_value_compatible(param.default, param.annotation): raise ValueError(default_value_error_msg) diff --git a/tests/unittests/tools/test_build_function_declaration.py b/tests/unittests/tools/test_build_function_declaration.py index 608f06cb13..cfaeaa765d 100644 --- a/tests/unittests/tools/test_build_function_declaration.py +++ b/tests/unittests/tools/test_build_function_declaration.py @@ -102,6 +102,53 @@ def simple_function(input_str: dict[str, str]) -> str: assert function_decl.name == 'simple_function' assert function_decl.parameters.type == 'OBJECT' assert function_decl.parameters.properties['input_str'].type == 'OBJECT' + assert ( + function_decl.parameters.properties[ + 'input_str' + ].additional_properties.type + == 'STRING' + ) + + +def test_dict_input_with_int_values(): + def simple_function(input_str: dict[str, int]) -> str: + return {'result': input_str} + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=simple_function + ) + + assert function_decl.name == 'simple_function' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['input_str'].type == 'OBJECT' + assert ( + function_decl.parameters.properties[ + 'input_str' + ].additional_properties.type + == 'INTEGER' + ) + + +def test_list_of_dict_input(): + """Test list[dict[str, str]] emits proper schema with additional_properties.""" + + def simple_function(fruits: list[dict[str, str]]) -> str: + return str(fruits) + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=simple_function + ) + + assert function_decl.name == 'simple_function' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['fruits'].type == 'ARRAY' + assert function_decl.parameters.properties['fruits'].items.type == 'OBJECT' + assert ( + function_decl.parameters.properties[ + 'fruits' + ].items.additional_properties.type + == 'STRING' + ) def test_basemodel_input(): @@ -279,6 +326,12 @@ def simple_function( assert function_decl.parameters.properties['input_str'].items.type == 'STRING' assert function_decl.parameters.properties['input_dir'].type == 'ARRAY' assert function_decl.parameters.properties['input_dir'].items.type == 'OBJECT' + assert ( + function_decl.parameters.properties[ + 'input_dir' + ].items.additional_properties.type + == 'STRING' + ) def test_enums():