Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/google/adk/flows/llm_flows/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def _build_task_input_user_content(
try:
import json as _json

text = _json.dumps(dict(fc.args))
text = _json.dumps(dict(fc.args), ensure_ascii=False)
except (TypeError, ValueError):
text = str(fc.args)
parts = [types.Part(text=text)]
Expand Down
4 changes: 2 additions & 2 deletions src/google/adk/utils/content_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def to_user_content(value: Any) -> types.Content:
deep-copied)
- str -> single text part
- BaseModel -> model_dump_json() text part
- dict/list -> json.dumps() text part
- dict/list -> json.dumps() text part (non-ASCII preserved, not escaped)
- anything else -> str() text part
"""
if isinstance(value, types.Content):
Expand All @@ -74,7 +74,7 @@ def to_user_content(value: Any) -> types.Content:
elif isinstance(value, BaseModel):
text = value.model_dump_json()
elif isinstance(value, (dict, list)):
text = json.dumps(value)
text = json.dumps(value, ensure_ascii=False)
else:
text = str(value)
return types.Content(role='user', parts=[types.Part(text=text)])
38 changes: 38 additions & 0 deletions tests/unittests/flows/llm_flows/test_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1894,3 +1894,41 @@ def test_recover_compacted_parallel_call_reinjects_sibling_response():
if part.function_response
}
assert response_ids == {"lr-1", "reg-1"}


def test_task_input_user_content_preserves_non_ascii():
"""Delegated task input must not escape non-ASCII FC args (issue #6279).

A chat coordinator delegates to a task sub-agent via a function call; the
task agent's first user turn is rebuilt from the FC args. Escaping non-Latin
characters to ``\\uXXXX`` there bloats prompt tokens and degrades responses.
"""
fc_id = "fc_task_1"
events = [
Event(
invocation_id="inv1",
author="coordinator",
content=types.Content(
role="model",
parts=[
types.Part(
function_call=types.FunctionCall(
id=fc_id,
name="delegate",
args={"query": "שלום עולם", "city": "北京"},
)
)
],
),
),
]

content = contents._build_task_input_user_content( # pylint: disable=protected-access
events, isolation_scope=fc_id
)

assert content is not None and content.parts
text = content.parts[0].text
assert "שלום עולם" in text
assert "北京" in text
assert "\\u" not in text
22 changes: 22 additions & 0 deletions tests/unittests/utils/test_content_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,25 @@ def test_to_user_content_other_input_is_str():
content = to_user_content(42)
assert content.role == 'user'
assert content.parts[0].text == '42'


def test_to_user_content_dict_input_preserves_non_ascii():
"""Non-ASCII input must reach the LLM as-is, not as \\uXXXX escapes.

Escaping (json.dumps' default ensure_ascii=True) turns each non-Latin
character into a ``\\uXXXX`` sequence, which bloats prompt tokens and
degrades model responses for non-English inputs (issue #6279).
"""
content = to_user_content({'query': 'שלום עולם', 'city': '北京'})
text = content.parts[0].text
assert 'שלום עולם' in text
assert '北京' in text
assert '\\u' not in text


def test_to_user_content_list_input_preserves_non_ascii():
content = to_user_content(['שלום', '你好'])
text = content.parts[0].text
assert 'שלום' in text
assert '你好' in text
assert '\\u' not in text