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 5d658df67a..b90b069523 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'}