Skip to content

Commit 1d7fdad

Browse files
committed
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
1 parent 684a6e7 commit 1d7fdad

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

src/google/adk/tools/set_model_response_tool.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,22 @@ def set_model_response() -> str:
8787
annotation=list[inner_type],
8888
)
8989
]
90+
elif isinstance(output_schema, dict):
91+
# For raw dict schemas (e.g. {"type": "object", "properties": {...}}),
92+
# use the `dict` type itself as the annotation rather than the dict
93+
# instance. Passing the instance would later trigger
94+
# `annotation in _py_builtin_type_to_schema_type.keys()` inside
95+
# `_function_parameter_parse_util`, which calls `__hash__` on the
96+
# annotation and raises `TypeError: unhashable type: 'dict'`.
97+
params = [
98+
inspect.Parameter(
99+
'response',
100+
inspect.Parameter.KEYWORD_ONLY,
101+
annotation=dict,
102+
)
103+
]
90104
else:
91-
# For other schema types (list[str], dict, etc.),
105+
# For other schema types (list[str], dict[str, int], etc.),
92106
# create a single parameter with the actual schema type
93107
params = [
94108
inspect.Parameter(

tests/unittests/tools/test_set_model_response_tool.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,87 @@ async def test_run_async_dict_schema():
467467
assert result is not None
468468
assert isinstance(result, dict)
469469
assert result == {'a': 1, 'b': 2, 'c': 3}
470+
471+
472+
# Regression tests for raw dict output_schema (issue #5469)
473+
474+
475+
def test_tool_initialization_raw_dict_schema():
476+
"""Raw dict output_schema must not crash and must be stored as-is."""
477+
raw_schema = {
478+
'type': 'object',
479+
'properties': {'result': {'type': 'string'}},
480+
}
481+
482+
tool = SetModelResponseTool(raw_schema)
483+
484+
assert tool.output_schema == raw_schema
485+
assert not tool._is_basemodel
486+
assert not tool._is_list_of_basemodel
487+
assert tool.name == 'set_model_response'
488+
assert tool.func is not None
489+
490+
491+
def test_function_signature_generation_raw_dict_schema():
492+
"""Raw dict schemas should produce a single `response: dict` parameter.
493+
494+
The annotation must be the `dict` type (hashable), not the dict instance,
495+
so downstream `_is_builtin_primitive_or_compound` does not raise
496+
`TypeError: unhashable type: 'dict'`.
497+
"""
498+
raw_schema = {
499+
'type': 'object',
500+
'properties': {'result': {'type': 'string'}},
501+
}
502+
503+
tool = SetModelResponseTool(raw_schema)
504+
505+
sig = inspect.signature(tool.func)
506+
507+
assert 'response' in sig.parameters
508+
assert len(sig.parameters) == 1
509+
assert sig.parameters['response'].kind == inspect.Parameter.KEYWORD_ONLY
510+
# The annotation is the hashable `dict` type, not the dict instance.
511+
assert sig.parameters['response'].annotation is dict
512+
513+
514+
def test_get_declaration_raw_dict_schema():
515+
"""`_get_declaration` must not raise when given a raw dict schema.
516+
517+
This is the original failure mode reported in issue #5469: building the
518+
function declaration triggered `TypeError: unhashable type: 'dict'`
519+
because the dict instance was used as an annotation.
520+
"""
521+
raw_schema = {
522+
'type': 'object',
523+
'properties': {'result': {'type': 'string'}},
524+
}
525+
526+
tool = SetModelResponseTool(raw_schema)
527+
528+
declaration = tool._get_declaration()
529+
530+
assert declaration is not None
531+
assert declaration.name == 'set_model_response'
532+
assert declaration.description is not None
533+
534+
535+
@pytest.mark.asyncio
536+
async def test_run_async_raw_dict_schema():
537+
"""Tool execution with a raw dict schema returns the response unchanged."""
538+
raw_schema = {
539+
'type': 'object',
540+
'properties': {'result': {'type': 'string'}},
541+
}
542+
tool = SetModelResponseTool(raw_schema)
543+
544+
agent = LlmAgent(name='test_agent', model='gemini-1.5-flash')
545+
invocation_context = await _create_invocation_context(agent)
546+
tool_context = ToolContext(invocation_context)
547+
548+
result = await tool.run_async(
549+
args={'response': {'result': 'hello'}},
550+
tool_context=tool_context,
551+
)
552+
553+
assert result == {'result': 'hello'}

0 commit comments

Comments
 (0)