[fix] Handle escaped characters in GLM tool call parser to prevent double serialization (#12456)

This commit is contained in:
soaringk
2025-11-05 08:48:14 +08:00
committed by GitHub
parent fb9582c4e1
commit 44da737770
2 changed files with 127 additions and 13 deletions

View File

@@ -2191,6 +2191,109 @@ class TestGlm4MoeDetector(unittest.TestCase):
)
self.assertEqual(self.detector._buffer, "")
def test_array_argument_with_escaped_json(self):
"""Test that array arguments with escaped JSON are properly handled without double-escaping."""
# Add a tool with array parameter
tools_with_array = [
Tool(
type="function",
function=Function(
name="todo_write",
description="Write todos",
parameters={
"type": "object",
"properties": {
"todos": {
"type": "array",
"description": "The updated todo list",
}
},
"required": ["todos"],
},
),
),
]
def check_params(result):
self.assertEqual(1, len(result.calls))
self.assertEqual("todo_write", result.calls[0].name)
params = json.loads(result.calls[0].parameters)
self.assertIsInstance(params["todos"], list)
self.assertEqual(4, len(params["todos"]))
self.assertEqual("1", params["todos"][0]["id"])
self.assertEqual(
"Check for hard-coded issues in the backend code",
params["todos"][0]["task"],
)
self.assertEqual("in_progress", params["todos"][0]["status"])
self.assertEqual("2", params["todos"][1]["id"])
self.assertEqual(
"Check for hard-coded issues in the frontend code",
params["todos"][1]["task"],
)
self.assertEqual("pending", params["todos"][1]["status"])
self.assertEqual("3", params["todos"][2]["id"])
self.assertEqual(
"Check for code violating the Single Responsibility Principle",
params["todos"][2]["task"],
)
self.assertEqual("pending", params["todos"][2]["status"])
self.assertEqual("4", params["todos"][3]["id"])
self.assertEqual(
"Generate a rectification proposal report", params["todos"][3]["task"]
)
self.assertEqual("pending", params["todos"][3]["status"])
# Simulate the raw response from GLM-4.6 model with normal and escaped JSON in XML
result = self.detector.detect_and_parse(
"""<tool_call>todo_write\n<arg_key>todos</arg_key>\n<arg_value>[{\"id\": \"1\", \"task\": \"Check for hard-coded issues in the backend code\", \"status\": \"in_progress\"}, {\"id\": \"2\", \"task\": \"Check for hard-coded issues in the frontend code\", \"status\": \"pending\"}, {\"id\": \"3\", \"task\": \"Check for code violating the Single Responsibility Principle\", \"status\": \"pending\"}, {\"id\": \"4\", \"task\": \"Generate a rectification proposal report\", \"status\": \"pending\"}]</arg_value>
</tool_call>""",
tools_with_array,
)
check_params(result)
result = self.detector.detect_and_parse(
r"""<tool_call>todo_write\n<arg_key>todos</arg_key>\n<arg_value>[{\"id\": \"1\", \"task\": \"Check for hard-coded issues in the backend code\", \"status\": \"in_progress\"}, {\"id\": \"2\", \"task\": \"Check for hard-coded issues in the frontend code\", \"status\": \"pending\"}, {\"id\": \"3\", \"task\": \"Check for code violating the Single Responsibility Principle\", \"status\": \"pending\"}, {\"id\": \"4\", \"task\": \"Generate a rectification proposal report\", \"status\": \"pending\"}]</arg_value>
</tool_call>""",
tools_with_array,
)
check_params(result)
def check_single_todos(tool_result, expected):
self.assertEqual(1, len(tool_result.calls))
self.assertEqual("todo_write", tool_result.calls[0].name)
params = json.loads(tool_result.calls[0].parameters)
self.assertIsInstance(params["todos"], list)
self.assertEqual(1, len(params["todos"]))
self.assertEqual("1", params["todos"][0]["id"])
self.assertEqual(expected, params["todos"][0]["task"])
self.assertEqual("pending", params["todos"][0]["status"])
# Test with escaped JSON containing backslashes in content (e.g., Windows paths)
expected_path = r"Check file at C:\Users\test.txt"
result = self.detector.detect_and_parse(
"""<tool_call>todo_write\n<arg_key>todos</arg_key>\n<arg_value>[{\"id\": \"1\", \"task\": \"Check file at C:\\\\Users\\\\test.txt\", \"status\": \"pending\"}]</arg_value></tool_call>""",
tools_with_array,
)
check_single_todos(result, expected_path)
result = self.detector.detect_and_parse(
r"""<tool_call>todo_write\n<arg_key>todos</arg_key>\n<arg_value>[{\"id\": \"1\", \"task\": \"Check file at C:\\\\Users\\\\test.txt\", \"status\": \"pending\"}]</arg_value></tool_call>""",
tools_with_array,
)
check_single_todos(result, expected_path)
# Should contain literal \n, not actual newline
expected_output = r"Print \n to see newline"
result = self.detector.detect_and_parse(
"""<tool_call>todo_write\n<arg_key>todos</arg_key>\n<arg_value>[{\"id\": \"1\", \"task\": \"Print \\\\n to see newline\",\"status\": \"pending\"}]</arg_value></tool_call>""",
tools_with_array,
)
check_single_todos(result, expected_output)
result = self.detector.detect_and_parse(
r"""<tool_call>todo_write\n<arg_key>todos</arg_key>\n<arg_value>[{\"id\": \"1\", \"task\": \"Print \\\\n to see newline\",\"status\": \"pending\"}]</arg_value></tool_call>""",
tools_with_array,
)
check_single_todos(result, expected_output)
class TestJsonArrayParser(unittest.TestCase):
def setUp(self):