Skip to content

Commit d69dedd

Browse files
MukundaKattaxuanyang15
authored andcommitted
fix(tools): accept dict output_schema in SetModelResponseTool (#5469)
Merge #5516 ## Summary Fixes #5469. When `output_schema` is a raw dict (e.g. `{"type": "object", "properties": {...}}`), `SetModelResponseTool.__init__` previously fell through to the generic `else` branch and used the dict **instance** 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'`. This change adds an explicit `elif isinstance(output_schema, dict)` branch that uses the `dict` type (hashable) as the annotation, so the existing builtin lookup maps it to `Type.OBJECT` cleanly. `run_async` already handles this case via the existing `args.get('response')` pass-through. ## Reproduction (before fix) ```python from google.adk.agents import Agent agent = Agent( name="test", model="gemini-2.5-flash", instruction="You are a helpful agent.", output_schema={"type": "object", "properties": {"result": {"type": "string"}}}, ) # -> TypeError: unhashable type: 'dict' ``` ## Testing plan - Added regression unit tests in `tests/unittests/tools/test_set_model_response_tool.py`: - `test_tool_initialization_raw_dict_schema` — `__init__` with a raw dict does not crash and stores the schema. - `test_function_signature_generation_raw_dict_schema` — generated signature has a single `response: dict` parameter (annotation is the `dict` type, not the dict instance). - `test_get_declaration_raw_dict_schema` — `_get_declaration()` returns a valid declaration without raising `TypeError`. - `test_run_async_raw_dict_schema` — `run_async` returns the response unchanged. - Run: `pytest tests/unittests/tools/test_set_model_response_tool.py -q` - Existing tests for `BaseModel`, `list[BaseModel]`, `list[str]`, `dict[str, int]` schemas remain unchanged and still pass. ## Notes - Schema fidelity (propagating dict-schema constraints into the function declaration) is intentionally out of scope; this PR only fixes the crash, matching the issue's reported scope and the existing handling for `list[str]` / `dict[str, int]`. Co-authored-by: Xuan Yang <xygoogle@google.com> COPYBARA_INTEGRATE_REVIEW=#5516 from MukundaKatta:fix/set-model-response-tool-dict-schema 5e808a9 PiperOrigin-RevId: 941169611
1 parent d60ac69 commit d69dedd

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

src/google/adk/tools/set_model_response_tool.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def __init__(self, output_schema: SchemaType):
5252
- dict: Raw dict schemas
5353
- Schema: Google's Schema type
5454
"""
55+
# Convert types.Schema instance to raw dict to avoid unhashable crash
56+
if isinstance(output_schema, types.Schema):
57+
output_schema = output_schema.model_dump(exclude_none=True)
58+
5559
self.output_schema = output_schema
5660
self._is_basemodel = is_basemodel_schema(output_schema)
5761
self._is_list_of_basemodel = is_list_of_basemodel(output_schema)
@@ -87,8 +91,22 @@ def set_model_response() -> str:
8791
annotation=list[inner_type],
8892
)
8993
]
94+
elif isinstance(output_schema, dict):
95+
# For raw dict schemas (e.g. {"type": "object", "properties": {...}}),
96+
# use the `dict` type itself as the annotation rather than the dict
97+
# instance. Passing the instance would later trigger
98+
# `annotation in _py_builtin_type_to_schema_type.keys()` inside
99+
# `_function_parameter_parse_util`, which calls `__hash__` on the
100+
# annotation and raises `TypeError: unhashable type: 'dict'`.
101+
params = [
102+
inspect.Parameter(
103+
'response',
104+
inspect.Parameter.KEYWORD_ONLY,
105+
annotation=dict,
106+
)
107+
]
90108
else:
91-
# For other schema types (list[str], dict, etc.),
109+
# For other schema types (list[str], dict[str, int], etc.),
92110
# create a single parameter with the actual schema type
93111
params = [
94112
inspect.Parameter(

tests/unittests/tools/test_set_model_response_tool.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from google.adk.sessions.in_memory_session_service import InMemorySessionService
2626
from google.adk.tools.set_model_response_tool import SetModelResponseTool
2727
from google.adk.tools.tool_context import ToolContext
28+
from google.genai import types
2829
from pydantic import BaseModel
2930
from pydantic import Field
3031
from pydantic import ValidationError
@@ -472,6 +473,105 @@ async def test_run_async_dict_schema():
472473
assert result == {'a': 1, 'b': 2, 'c': 3}
473474

474475

476+
def test_tool_initialization_raw_dict_schema():
477+
"""Raw dict output_schema must not crash and must be stored as-is."""
478+
raw_schema = {
479+
'type': 'object',
480+
'properties': {'result': {'type': 'string'}},
481+
}
482+
483+
tool = SetModelResponseTool(raw_schema)
484+
485+
assert tool.output_schema == raw_schema
486+
assert not tool._is_basemodel
487+
assert not tool._is_list_of_basemodel
488+
assert tool.name == 'set_model_response'
489+
assert tool.func is not None
490+
491+
492+
def test_function_signature_generation_raw_dict_schema():
493+
"""Raw dict schemas should produce a single `response: dict` parameter.
494+
495+
The annotation must be the `dict` type (hashable), not the dict instance,
496+
so downstream `_is_builtin_primitive_or_compound` does not raise
497+
`TypeError: unhashable type: 'dict'`.
498+
"""
499+
raw_schema = {
500+
'type': 'object',
501+
'properties': {'result': {'type': 'string'}},
502+
}
503+
504+
tool = SetModelResponseTool(raw_schema)
505+
506+
sig = inspect.signature(tool.func)
507+
508+
assert 'response' in sig.parameters
509+
assert len(sig.parameters) == 1
510+
assert sig.parameters['response'].kind == inspect.Parameter.KEYWORD_ONLY
511+
# The annotation is the hashable `dict` type, not the dict instance.
512+
assert sig.parameters['response'].annotation is dict
513+
514+
515+
def test_get_declaration_raw_dict_schema():
516+
"""`_get_declaration` must not raise when given a raw dict schema."""
517+
raw_schema = {
518+
'type': 'object',
519+
'properties': {'result': {'type': 'string'}},
520+
}
521+
522+
tool = SetModelResponseTool(raw_schema)
523+
524+
declaration = tool._get_declaration()
525+
526+
assert declaration is not None
527+
assert declaration.name == 'set_model_response'
528+
assert declaration.description is not None
529+
530+
531+
@pytest.mark.asyncio
532+
async def test_run_async_raw_dict_schema():
533+
"""Tool execution with a raw dict schema returns the response unchanged."""
534+
raw_schema = {
535+
'type': 'object',
536+
'properties': {'result': {'type': 'string'}},
537+
}
538+
tool = SetModelResponseTool(raw_schema)
539+
540+
agent = LlmAgent(name='test_agent', model='gemini-1.5-flash')
541+
invocation_context = await _create_invocation_context(agent)
542+
tool_context = ToolContext(invocation_context)
543+
544+
result = await tool.run_async(
545+
args={'response': {'result': 'hello'}},
546+
tool_context=tool_context,
547+
)
548+
549+
assert result == {'result': 'hello'}
550+
551+
552+
def test_tool_initialization_schema_instance():
553+
"""types.Schema instance output_schema must be converted to dict and not crash."""
554+
schema_instance = types.Schema(
555+
type=types.Type.OBJECT,
556+
properties={'result': types.Schema(type=types.Type.STRING)},
557+
)
558+
559+
tool = SetModelResponseTool(schema_instance)
560+
561+
# Check that it converted it to a dictionary
562+
assert isinstance(tool.output_schema, dict)
563+
assert 'result' in tool.output_schema['properties']
564+
565+
sig = inspect.signature(tool.func)
566+
assert 'response' in sig.parameters
567+
assert sig.parameters['response'].annotation is dict
568+
569+
# Check that get_declaration works and doesn't crash with TypeError
570+
declaration = tool._get_declaration()
571+
assert declaration is not None
572+
assert declaration.name == 'set_model_response'
573+
574+
475575
class SubSchema(BaseModel):
476576

477577
field1: str = Field(description='Field 1')

0 commit comments

Comments
 (0)