Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/google/adk/tools/set_model_response_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
84 changes: 84 additions & 0 deletions tests/unittests/tools/test_set_model_response_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}