Skip to content

Commit 85f397d

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: avoid pre-serializing dict values in Interactions API to prevent double-escaping
PiperOrigin-RevId: 916037238
1 parent 115124c commit 85f397d

2 files changed

Lines changed: 39 additions & 12 deletions

File tree

src/google/adk/models/interactions_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ def convert_part_to_interaction_content(part: types.Part) -> Optional[dict]:
111111
).decode('utf-8')
112112
return result
113113
elif part.function_response is not None:
114-
# Convert the function response to a string for the interactions API
115-
# The interactions API expects result to be either a string or items list
114+
# Pass the function response through to the interactions API.
115+
# Dict and list values are passed directly — the Interactions API handles
116+
# JSON serialization internally. Pre-serializing with json.dumps() would
117+
# cause double-escaping.
116118
result = part.function_response.response
117-
if isinstance(result, dict):
118-
result = json.dumps(result)
119-
elif not isinstance(result, str):
119+
if not isinstance(result, (dict, str, list)):
120120
result = str(result)
121121
logger.debug(
122122
'Converting function_response: name=%s, call_id=%s',

tests/unittests/models/test_interactions_utils.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,9 @@ def test_function_response_dict(self):
280280
assert result['type'] == 'function_result'
281281
assert result['call_id'] == 'call_123'
282282
assert result['name'] == 'get_weather'
283-
# Dict should be JSON serialized
284-
assert json.loads(result['result']) == {
285-
'temperature': 20,
286-
'condition': 'sunny',
287-
}
283+
# Dict should be passed through directly (not JSON-serialized).
284+
assert result['result'] == {'temperature': 20, 'condition': 'sunny'}
285+
assert isinstance(result['result'], dict)
288286

289287
def test_function_response_simple(self):
290288
"""Test converting a function response Part with simple response."""
@@ -299,8 +297,37 @@ def test_function_response_simple(self):
299297
assert result['type'] == 'function_result'
300298
assert result['call_id'] == 'call_123'
301299
assert result['name'] == 'check_weather'
302-
# Dict should be JSON serialized
303-
assert json.loads(result['result']) == {'message': 'Weather is sunny'}
300+
# Dict should be passed through directly (not JSON-serialized).
301+
assert result['result'] == {'message': 'Weather is sunny'}
302+
303+
def test_function_response_dict_not_double_serialized(self):
304+
"""Regression test: avoid double-serializing bash tool outputs.
305+
306+
Bash tool responses contain JSON structures (stdout/stderr). When these
307+
dict responses were json.dumps()'d before being sent to the Interactions
308+
API, the API's own serialization would escape the already-escaped content,
309+
producing unreadable output like:
310+
{"result":"\\\"{\\\\\\\"error\\\\\\\":\\\\\\\"...\\\\\\\"}\\\""
311+
"""
312+
bash_response = {
313+
'stdout': '{"name": "test", "version": "1.0"}\n',
314+
'stderr': '',
315+
}
316+
part = types.Part(
317+
function_response=types.FunctionResponse(
318+
id='call_bash',
319+
name='bash',
320+
response=bash_response,
321+
)
322+
)
323+
result = interactions_utils.convert_part_to_interaction_content(part)
324+
# The result value must be the dict itself, NOT a JSON string.
325+
assert isinstance(result['result'], dict)
326+
assert result['result'] == bash_response
327+
# Verify there's no double-escaping: if result were a JSON string,
328+
# serializing it again would add backslashes before the internal quotes.
329+
wire_json = json.dumps(result)
330+
assert '\\\\' not in wire_json
304331

305332
def test_inline_data_image(self):
306333
"""Test converting an inline image Part."""

0 commit comments

Comments
 (0)