Skip to content
Open
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
4 changes: 2 additions & 2 deletions python/packages/core/agent_framework/_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions python/packages/core/agent_framework/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion python/packages/core/agent_framework/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
44 changes: 44 additions & 0 deletions python/packages/core/tests/core/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
34 changes: 34 additions & 0 deletions python/packages/core/tests/core/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Loading