Skip to content

Commit a3ecbe5

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 dec2182 commit a3ecbe5

4 files changed

Lines changed: 79 additions & 2 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
@@ -360,7 +360,7 @@ def _build_task_input_user_content(
360360
try:
361361
import json as _json
362362

363-
text = _json.dumps(dict(fc.args))
363+
text = _json.dumps(dict(fc.args), ensure_ascii=False)
364364
except (TypeError, ValueError):
365365
text = str(fc.args)
366366
parts = [types.Part(text=text)]

src/google/adk/workflow/_llm_agent_wrapper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def _node_input_to_content(node_input: Any) -> types.Content:
194194
elif isinstance(node_input, BaseModel):
195195
text = node_input.model_dump_json()
196196
elif isinstance(node_input, (dict, list)):
197-
text = json.dumps(node_input)
197+
text = json.dumps(node_input, ensure_ascii=False)
198198
else:
199199
text = str(node_input)
200200
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
@@ -1598,3 +1598,41 @@ def test_rearrange_async_function_responses_early_returns_when_no_responses():
15981598
events
15991599
)
16001600
assert result is events
1601+
1602+
1603+
def test_task_input_user_content_preserves_non_ascii():
1604+
"""Delegated task input must not escape non-ASCII FC args (issue #6279).
1605+
1606+
A chat coordinator delegates to a task sub-agent via a function call; the
1607+
task agent's first user turn is rebuilt from the FC args. Escaping non-Latin
1608+
characters to ``\\uXXXX`` there bloats prompt tokens and degrades responses.
1609+
"""
1610+
fc_id = "fc_task_1"
1611+
events = [
1612+
Event(
1613+
invocation_id="inv1",
1614+
author="coordinator",
1615+
content=types.Content(
1616+
role="model",
1617+
parts=[
1618+
types.Part(
1619+
function_call=types.FunctionCall(
1620+
id=fc_id,
1621+
name="delegate",
1622+
args={"query": "שלום עולם", "city": "北京"},
1623+
)
1624+
)
1625+
],
1626+
),
1627+
),
1628+
]
1629+
1630+
content = contents._build_task_input_user_content( # pylint: disable=protected-access
1631+
events, isolation_scope=fc_id
1632+
)
1633+
1634+
assert content is not None and content.parts
1635+
text = content.parts[0].text
1636+
assert "שלום עולם" in text
1637+
assert "北京" in text
1638+
assert "\\u" not in text

tests/unittests/workflow/test_llm_agent_as_node.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,45 @@ async def test_single_turn_input_event_inherits_branch_and_scope(
234234
assert event.isolation_scope == 'scope-1'
235235

236236

237+
# --- _node_input_to_content non-ASCII handling ---
238+
239+
240+
class TestNodeInputNonAscii:
241+
"""Node input must reach the LLM without escaping non-ASCII characters.
242+
243+
Escaping (e.g. json.dumps' default ensure_ascii=True) turns each non-Latin
244+
character into a ``\\uXXXX`` sequence, which bloats prompt tokens and
245+
degrades model responses for non-English inputs (issue #6279).
246+
"""
247+
248+
def _text_of(self, node_input: Any) -> str:
249+
from google.adk.workflow._llm_agent_wrapper import _node_input_to_content
250+
251+
content = _node_input_to_content(node_input)
252+
assert content.parts and content.parts[0].text is not None
253+
return content.parts[0].text
254+
255+
def test_dict_input_preserves_non_ascii(self):
256+
text = self._text_of({'query': 'שלום עולם', 'city': '北京'})
257+
assert 'שלום עולם' in text
258+
assert '北京' in text
259+
assert '\\u' not in text
260+
261+
def test_list_input_preserves_non_ascii(self):
262+
text = self._text_of(['שלום', '你好'])
263+
assert 'שלום' in text
264+
assert '你好' in text
265+
assert '\\u' not in text
266+
267+
def test_base_model_input_preserves_non_ascii(self):
268+
class _Payload(BaseModel):
269+
topic: str
270+
271+
text = self._text_of(_Payload(topic='עברית 中文'))
272+
assert 'עברית 中文' in text
273+
assert '\\u' not in text
274+
275+
237276
# --- build_node auto-wrapping ---
238277

239278

0 commit comments

Comments
 (0)