diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index c4f93237f03..091342c5243 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -1304,7 +1304,7 @@ def _parse_prompt_result_from_mcp( return "" if len(parts) == 1: return parts[0] - return json.dumps(parts, default=str) + return json.dumps(parts, default=str, ensure_ascii=False) def _parse_message_from_mcp( self, @@ -1390,7 +1390,7 @@ def _parse_tool_result_from_mcp( structured_block: Content | None = None if mcp_type.structured_content is not None: structured_block = Content.from_text( - json.dumps(mcp_type.structured_content, default=str), **additional_kwargs + json.dumps(mcp_type.structured_content, default=str, ensure_ascii=False), **additional_kwargs ) # Select model-visible content per explicit policy (#7866). Host payload still diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 59aaf472803..3436bbcd25c 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1413,13 +1413,15 @@ def parse_result(result: Any) -> list[Content]: parsed_items.append(item) else: dumpable = FunctionTool._make_dumpable(item) - text = dumpable if isinstance(dumpable, str) else json.dumps(dumpable, default=str) + text = ( + dumpable if isinstance(dumpable, str) else json.dumps(dumpable, default=str, ensure_ascii=False) + ) parsed_items.append(Content.from_text(text)) return parsed_items dumpable = FunctionTool._make_dumpable(result) if isinstance(dumpable, str): return [Content.from_text(dumpable)] - return [Content.from_text(json.dumps(dumpable, default=str))] + return [Content.from_text(json.dumps(dumpable, default=str, ensure_ascii=False))] def to_json_schema_spec(self) -> dict[str, Any]: """Convert a FunctionTool to the JSON Schema function specification format. diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 629593b8839..7b55a4fddcf 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1202,7 +1202,7 @@ def from_function_result( items_list = [Content.from_text(result)] elif result is not None: try: - text = json.dumps(result, default=str) + text = json.dumps(result, default=str, ensure_ascii=False) except (TypeError, ValueError): text = str(result) items_list = [Content.from_text(text)] diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 757e2a15f88..2f173abd7ae 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -1104,6 +1104,31 @@ def test_mcp_tool_str_and_parse_prompt_result_rich_content() -> None: } +def test_parse_prompt_result_from_mcp_unicode_unescaped(): + """Test that non-ASCII unicode characters in multi-message prompt results are not escaped to \\uXXXX.""" + tool = _HELPER_MCP_TOOL + prompt_result = types.GetPromptResult( + description="Unicode prompt", + messages=[ + types.PromptMessage( + role="user", + content=types.TextContent(type="text", text="Año"), + ), + types.PromptMessage( + role="assistant", + content=types.TextContent(type="text", text="Género"), + ), + ], + ) + + result = tool._parse_prompt_result_from_mcp(prompt_result) + + assert "Año" in result + assert "Género" in result + assert "\\u00f1" not in result + assert "\\u00e9" not in result + + def test_parse_tool_result_from_mcp(): """Test conversion from MCP tool result with images preserves original order.""" mcp_result = types.CallToolResult( @@ -1254,6 +1279,25 @@ def test_parse_tool_result_from_mcp_structured_content_only(): assert parsed == {"Tables": [{"Name": "Sales", "Columns": ["Amount", "Date"]}]} +def test_parse_tool_result_from_mcp_structured_content_unicode_unescaped(): + """Test that non-ASCII unicode characters in structured_content are not escaped to \\uXXXX.""" + mcp_result = types.CallToolResult( + content=[], + structured_content={"columns": ["Año", "Género", "Categoría"]}, + ) + result = _HELPER_MCP_TOOL._parse_tool_result_from_mcp(mcp_result) + + assert isinstance(result, list) + assert len(result) == 1 + assert result[0].text is not None + assert "Año" in result[0].text + assert "Género" in result[0].text + assert "Categoría" in result[0].text + assert "\\u00f1" not in result[0].text + assert "\\u00e9" not in result[0].text + assert "\\u00ed" not in result[0].text + + def test_parse_tool_result_from_mcp_structured_content_with_text(): """Default structured_first prefers structuredContent when both are present (#7866).""" mcp_result = types.CallToolResult( diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 771e94fb799..faa43d4c178 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -4653,6 +4653,40 @@ def test_parse_result_mixed_content_list(): assert result[1].type == "data" +def test_parse_result_non_ascii_unicode(): + """Test that non-ASCII unicode characters in dict results are not escaped to \\uXXXX.""" + data = {"columns": ["Año", "Género", "Categoría"]} + parsed = FunctionTool.parse_result(data) + assert len(parsed) == 1 + assert parsed[0].text is not None + assert "Año" in parsed[0].text + assert "Género" in parsed[0].text + assert "\\u00f1" not in parsed[0].text + assert "\\u00e9" not in parsed[0].text + + +def test_parse_result_list_non_ascii_unicode(): + """Test that non-ASCII unicode in list-of-items results are not escaped to \\uXXXX.""" + data = [{"name": "Año"}, {"name": "Género"}] + parsed = FunctionTool.parse_result(data) + assert len(parsed) == 1 + assert parsed[0].text is not None + assert "Año" in parsed[0].text + assert "Género" in parsed[0].text + assert "\\u00f1" not in parsed[0].text + + +def test_from_function_result_non_ascii_unicode(): + """Test that Content.from_function_result preserves UTF-8 unicode in dict results.""" + data = {"city": "München", "country": "España"} + content = Content.from_function_result(call_id="call-1", result=data) + assert content.result is not None + assert "München" in content.result + assert "España" in content.result + assert "\\u00fc" not in content.result + assert "\\u00f1" not in content.result + + def test_from_function_result_with_content_list(): """Test Content.from_function_result stores all items uniformly.""" content_list = [