Skip to content

Commit 6956737

Browse files
fix(workflow): preserve non-ASCII characters in LLM agent node input
Node input passed to an LLM agent (workflow node input and delegated-task function-call args) was serialized with json.dumps' default ensure_ascii=True, escaping non-Latin characters to \uXXXX. This bloats prompt tokens and degrades model responses for non-English inputs. Serialize with ensure_ascii=False so characters reach the model as-is, matching how the output-schema path already serializes responses.
1 parent 57e1ba6 commit 6956737

4 files changed

Lines changed: 63 additions & 3 deletions

File tree

src/google/adk/flows/llm_flows/contents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ def _build_task_input_user_content(
386386
try:
387387
import json as _json
388388

389-
text = _json.dumps(dict(fc.args))
389+
text = _json.dumps(dict(fc.args), ensure_ascii=False)
390390
except (TypeError, ValueError):
391391
text = str(fc.args)
392392
parts = [types.Part(text=text)]

src/google/adk/utils/content_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def to_user_content(value: Any) -> types.Content:
6464
deep-copied)
6565
- str -> single text part
6666
- BaseModel -> model_dump_json() text part
67-
- dict/list -> json.dumps() text part
67+
- dict/list -> json.dumps() text part (non-ASCII preserved, not escaped)
6868
- anything else -> str() text part
6969
"""
7070
if isinstance(value, types.Content):
@@ -74,7 +74,7 @@ def to_user_content(value: Any) -> types.Content:
7474
elif isinstance(value, BaseModel):
7575
text = value.model_dump_json()
7676
elif isinstance(value, (dict, list)):
77-
text = json.dumps(value)
77+
text = json.dumps(value, ensure_ascii=False)
7878
else:
7979
text = str(value)
8080
return types.Content(role='user', parts=[types.Part(text=text)])

tests/unittests/flows/llm_flows/test_contents.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,3 +1894,41 @@ def test_recover_compacted_parallel_call_reinjects_sibling_response():
18941894
if part.function_response
18951895
}
18961896
assert response_ids == {"lr-1", "reg-1"}
1897+
1898+
1899+
def test_task_input_user_content_preserves_non_ascii():
1900+
"""Delegated task input must not escape non-ASCII FC args (issue #6279).
1901+
1902+
A chat coordinator delegates to a task sub-agent via a function call; the
1903+
task agent's first user turn is rebuilt from the FC args. Escaping non-Latin
1904+
characters to ``\\uXXXX`` there bloats prompt tokens and degrades responses.
1905+
"""
1906+
fc_id = "fc_task_1"
1907+
events = [
1908+
Event(
1909+
invocation_id="inv1",
1910+
author="coordinator",
1911+
content=types.Content(
1912+
role="model",
1913+
parts=[
1914+
types.Part(
1915+
function_call=types.FunctionCall(
1916+
id=fc_id,
1917+
name="delegate",
1918+
args={"query": "שלום עולם", "city": "北京"},
1919+
)
1920+
)
1921+
],
1922+
),
1923+
),
1924+
]
1925+
1926+
content = contents._build_task_input_user_content( # pylint: disable=protected-access
1927+
events, isolation_scope=fc_id
1928+
)
1929+
1930+
assert content is not None and content.parts
1931+
text = content.parts[0].text
1932+
assert "שלום עולם" in text
1933+
assert "北京" in text
1934+
assert "\\u" not in text

tests/unittests/utils/test_content_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,25 @@ def test_to_user_content_other_input_is_str():
6666
content = to_user_content(42)
6767
assert content.role == 'user'
6868
assert content.parts[0].text == '42'
69+
70+
71+
def test_to_user_content_dict_input_preserves_non_ascii():
72+
"""Non-ASCII input must reach the LLM as-is, not as \\uXXXX escapes.
73+
74+
Escaping (json.dumps' default ensure_ascii=True) turns each non-Latin
75+
character into a ``\\uXXXX`` sequence, which bloats prompt tokens and
76+
degrades model responses for non-English inputs (issue #6279).
77+
"""
78+
content = to_user_content({'query': 'שלום עולם', 'city': '北京'})
79+
text = content.parts[0].text
80+
assert 'שלום עולם' in text
81+
assert '北京' in text
82+
assert '\\u' not in text
83+
84+
85+
def test_to_user_content_list_input_preserves_non_ascii():
86+
content = to_user_content(['שלום', '你好'])
87+
text = content.parts[0].text
88+
assert 'שלום' in text
89+
assert '你好' in text
90+
assert '\\u' not in text

0 commit comments

Comments
 (0)