diff --git a/src/google/adk/flows/llm_flows/contents.py b/src/google/adk/flows/llm_flows/contents.py index 62728124b81..a4aeba56c4d 100644 --- a/src/google/adk/flows/llm_flows/contents.py +++ b/src/google/adk/flows/llm_flows/contents.py @@ -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)] diff --git a/src/google/adk/utils/content_utils.py b/src/google/adk/utils/content_utils.py index 9f6e8d4a9e6..d0bada24015 100644 --- a/src/google/adk/utils/content_utils.py +++ b/src/google/adk/utils/content_utils.py @@ -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): @@ -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)]) diff --git a/tests/unittests/flows/llm_flows/test_contents.py b/tests/unittests/flows/llm_flows/test_contents.py index 930de059b77..479b279926c 100644 --- a/tests/unittests/flows/llm_flows/test_contents.py +++ b/tests/unittests/flows/llm_flows/test_contents.py @@ -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 diff --git a/tests/unittests/utils/test_content_utils.py b/tests/unittests/utils/test_content_utils.py index b4a97e3c4a4..b9bad110af9 100644 --- a/tests/unittests/utils/test_content_utils.py +++ b/tests/unittests/utils/test_content_utils.py @@ -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