From 1d7fdad5a128386bb13d54961077408a6e3ff17d Mon Sep 17 00:00:00 2001 From: mukunda katta Date: Mon, 27 Apr 2026 20:37:23 -0700 Subject: [PATCH] fix(tools): accept dict output_schema in SetModelResponseTool (#5469) When `output_schema` is a raw dict (e.g. `{"type": "object", "properties": {...}}`), `SetModelResponseTool` previously fell through to the generic else branch and used the dict instance itself as the parameter annotation. Downstream, `_function_parameter_parse_util._is_builtin_primitive_or_compound` does `annotation in _py_builtin_type_to_schema_type.keys()`, which calls `__hash__` on the annotation and raises `TypeError: unhashable type: 'dict'`. Detect raw dict schemas explicitly and use the `dict` type as the annotation, so the existing builtin lookup maps it to `Type.OBJECT` cleanly. `run_async` already handles this case via the existing pass-through branch. Fixes #5469 --- .../adk/tools/set_model_response_tool.py | 16 +++- .../tools/test_set_model_response_tool.py | 84 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/google/adk/tools/set_model_response_tool.py b/src/google/adk/tools/set_model_response_tool.py index d1dc6ed55d..90c330f23c 100644 --- a/src/google/adk/tools/set_model_response_tool.py +++ b/src/google/adk/tools/set_model_response_tool.py @@ -87,8 +87,22 @@ def set_model_response() -> str: annotation=list[inner_type], ) ] + elif isinstance(output_schema, dict): + # For raw dict schemas (e.g. {"type": "object", "properties": {...}}), + # use the `dict` type itself as the annotation rather than the dict + # instance. Passing the instance would later trigger + # `annotation in _py_builtin_type_to_schema_type.keys()` inside + # `_function_parameter_parse_util`, which calls `__hash__` on the + # annotation and raises `TypeError: unhashable type: 'dict'`. + params = [ + inspect.Parameter( + 'response', + inspect.Parameter.KEYWORD_ONLY, + annotation=dict, + ) + ] else: - # For other schema types (list[str], dict, etc.), + # For other schema types (list[str], dict[str, int], etc.), # create a single parameter with the actual schema type params = [ inspect.Parameter( diff --git a/tests/unittests/tools/test_set_model_response_tool.py b/tests/unittests/tools/test_set_model_response_tool.py index 89da394acc..4bad903b02 100644 --- a/tests/unittests/tools/test_set_model_response_tool.py +++ b/tests/unittests/tools/test_set_model_response_tool.py @@ -467,3 +467,87 @@ async def test_run_async_dict_schema(): assert result is not None assert isinstance(result, dict) assert result == {'a': 1, 'b': 2, 'c': 3} + + +# Regression tests for raw dict output_schema (issue #5469) + + +def test_tool_initialization_raw_dict_schema(): + """Raw dict output_schema must not crash and must be stored as-is.""" + raw_schema = { + 'type': 'object', + 'properties': {'result': {'type': 'string'}}, + } + + tool = SetModelResponseTool(raw_schema) + + assert tool.output_schema == raw_schema + assert not tool._is_basemodel + assert not tool._is_list_of_basemodel + assert tool.name == 'set_model_response' + assert tool.func is not None + + +def test_function_signature_generation_raw_dict_schema(): + """Raw dict schemas should produce a single `response: dict` parameter. + + The annotation must be the `dict` type (hashable), not the dict instance, + so downstream `_is_builtin_primitive_or_compound` does not raise + `TypeError: unhashable type: 'dict'`. + """ + raw_schema = { + 'type': 'object', + 'properties': {'result': {'type': 'string'}}, + } + + tool = SetModelResponseTool(raw_schema) + + sig = inspect.signature(tool.func) + + assert 'response' in sig.parameters + assert len(sig.parameters) == 1 + assert sig.parameters['response'].kind == inspect.Parameter.KEYWORD_ONLY + # The annotation is the hashable `dict` type, not the dict instance. + assert sig.parameters['response'].annotation is dict + + +def test_get_declaration_raw_dict_schema(): + """`_get_declaration` must not raise when given a raw dict schema. + + This is the original failure mode reported in issue #5469: building the + function declaration triggered `TypeError: unhashable type: 'dict'` + because the dict instance was used as an annotation. + """ + raw_schema = { + 'type': 'object', + 'properties': {'result': {'type': 'string'}}, + } + + tool = SetModelResponseTool(raw_schema) + + declaration = tool._get_declaration() + + assert declaration is not None + assert declaration.name == 'set_model_response' + assert declaration.description is not None + + +@pytest.mark.asyncio +async def test_run_async_raw_dict_schema(): + """Tool execution with a raw dict schema returns the response unchanged.""" + raw_schema = { + 'type': 'object', + 'properties': {'result': {'type': 'string'}}, + } + tool = SetModelResponseTool(raw_schema) + + agent = LlmAgent(name='test_agent', model='gemini-1.5-flash') + invocation_context = await _create_invocation_context(agent) + tool_context = ToolContext(invocation_context) + + result = await tool.run_async( + args={'response': {'result': 'hello'}}, + tool_context=tool_context, + ) + + assert result == {'result': 'hello'}