fix double Unicode escape issue in streaming tool_calls parameters (#13518)
Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
This commit is contained in:
@@ -249,7 +249,7 @@ class BaseFormatDetector(ABC):
|
|||||||
if cur_arguments:
|
if cur_arguments:
|
||||||
# Calculate how much of the arguments we've already streamed
|
# Calculate how much of the arguments we've already streamed
|
||||||
sent = len(self.streamed_args_for_tool[self.current_tool_id])
|
sent = len(self.streamed_args_for_tool[self.current_tool_id])
|
||||||
cur_args_json = json.dumps(cur_arguments)
|
cur_args_json = json.dumps(cur_arguments, ensure_ascii=False)
|
||||||
prev_arguments = None
|
prev_arguments = None
|
||||||
if self.current_tool_id < len(self.prev_tool_call_arr):
|
if self.current_tool_id < len(self.prev_tool_call_arr):
|
||||||
prev_arguments = self.prev_tool_call_arr[
|
prev_arguments = self.prev_tool_call_arr[
|
||||||
@@ -270,7 +270,7 @@ class BaseFormatDetector(ABC):
|
|||||||
|
|
||||||
# If the tool is still being parsed, send incremental changes
|
# If the tool is still being parsed, send incremental changes
|
||||||
elif prev_arguments:
|
elif prev_arguments:
|
||||||
prev_args_json = json.dumps(prev_arguments)
|
prev_args_json = json.dumps(prev_arguments, ensure_ascii=False)
|
||||||
if cur_args_json != prev_args_json:
|
if cur_args_json != prev_args_json:
|
||||||
prefix = _find_common_prefix(prev_args_json, cur_args_json)
|
prefix = _find_common_prefix(prev_args_json, cur_args_json)
|
||||||
argument_diff = prefix[sent:]
|
argument_diff = prefix[sent:]
|
||||||
|
|||||||
@@ -719,6 +719,140 @@ class TestBaseFormatDetector(unittest.TestCase):
|
|||||||
self.detector._buffer, "", "Buffer should be cleared for invalid tool"
|
self.detector._buffer, "", "Buffer should be cleared for invalid tool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_chinese_characters_not_double_escaped(self):
|
||||||
|
"""Test that Chinese characters in tool call parameters are not double-escaped."""
|
||||||
|
# Test with Chinese city name "杭州" (Hangzhou)
|
||||||
|
chunks = [
|
||||||
|
"<tool_call>",
|
||||||
|
'{"name": "get_weather", ',
|
||||||
|
'"arguments": {"city": "杭州"}}',
|
||||||
|
"</tool_call>",
|
||||||
|
]
|
||||||
|
|
||||||
|
accumulated_parameters = {}
|
||||||
|
for chunk in chunks:
|
||||||
|
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||||
|
if result.calls:
|
||||||
|
for call in result.calls:
|
||||||
|
if call.parameters:
|
||||||
|
tool_idx = call.tool_index if call.tool_index is not None else 0
|
||||||
|
if tool_idx not in accumulated_parameters:
|
||||||
|
accumulated_parameters[tool_idx] = ""
|
||||||
|
accumulated_parameters[tool_idx] += call.parameters
|
||||||
|
|
||||||
|
# Verify that Chinese characters are preserved (not escaped as \uXXXX)
|
||||||
|
self.assertGreater(
|
||||||
|
len(accumulated_parameters), 0, "Should have parsed parameters"
|
||||||
|
)
|
||||||
|
final_params_str = accumulated_parameters[0]
|
||||||
|
|
||||||
|
# The parameters string should contain the actual Chinese characters, not escaped Unicode
|
||||||
|
self.assertIn(
|
||||||
|
"杭州", final_params_str, "Should contain actual Chinese characters"
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"\\u676d", final_params_str, "Should not contain escaped Unicode sequences"
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"\\u5dde", final_params_str, "Should not contain escaped Unicode sequences"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the JSON can be parsed and contains the correct value
|
||||||
|
params = json.loads(final_params_str)
|
||||||
|
self.assertEqual(
|
||||||
|
params["city"], "杭州", "Should correctly parse Chinese city name"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_chinese_characters_incremental_streaming(self):
|
||||||
|
"""Test that Chinese characters work correctly with incremental streaming."""
|
||||||
|
# Test incremental streaming with Chinese characters
|
||||||
|
chunks = [
|
||||||
|
"<tool_call>",
|
||||||
|
'{"name": "get_weather", ',
|
||||||
|
'"arguments": {"city": "',
|
||||||
|
"杭州",
|
||||||
|
'"}}',
|
||||||
|
"</tool_call>",
|
||||||
|
]
|
||||||
|
|
||||||
|
accumulated_parameters = {}
|
||||||
|
for chunk in chunks:
|
||||||
|
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||||
|
if result.calls:
|
||||||
|
for call in result.calls:
|
||||||
|
if call.parameters:
|
||||||
|
tool_idx = call.tool_index if call.tool_index is not None else 0
|
||||||
|
if tool_idx not in accumulated_parameters:
|
||||||
|
accumulated_parameters[tool_idx] = ""
|
||||||
|
accumulated_parameters[tool_idx] += call.parameters
|
||||||
|
|
||||||
|
# Verify Chinese characters are preserved throughout streaming
|
||||||
|
self.assertGreater(
|
||||||
|
len(accumulated_parameters), 0, "Should have parsed parameters"
|
||||||
|
)
|
||||||
|
final_params_str = accumulated_parameters[0]
|
||||||
|
|
||||||
|
# Should contain actual Chinese characters, not escaped
|
||||||
|
self.assertIn(
|
||||||
|
"杭州", final_params_str, "Should contain actual Chinese characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse and verify
|
||||||
|
params = json.loads(final_params_str)
|
||||||
|
self.assertEqual(
|
||||||
|
params["city"], "杭州", "Should correctly parse Chinese city name"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_multiple_chinese_parameters(self):
|
||||||
|
"""Test multiple tool calls with Chinese parameters."""
|
||||||
|
# Test with multiple tool calls containing Chinese characters
|
||||||
|
chunks = [
|
||||||
|
"<tool_call>",
|
||||||
|
'{"name": "get_weather", "arguments": {"city": "北京"}}, ',
|
||||||
|
'{"name": "get_tourist_attractions", "arguments": {"city": "上海"}}',
|
||||||
|
"</tool_call>",
|
||||||
|
]
|
||||||
|
|
||||||
|
accumulated_parameters = {}
|
||||||
|
for chunk in chunks:
|
||||||
|
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||||
|
if result.calls:
|
||||||
|
for call in result.calls:
|
||||||
|
if call.parameters:
|
||||||
|
tool_idx = call.tool_index if call.tool_index is not None else 0
|
||||||
|
if tool_idx not in accumulated_parameters:
|
||||||
|
accumulated_parameters[tool_idx] = ""
|
||||||
|
accumulated_parameters[tool_idx] += call.parameters
|
||||||
|
|
||||||
|
# Verify both tool calls have correct Chinese characters
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
len(accumulated_parameters), 1, "Should have parsed parameters"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check first tool call (北京 - Beijing)
|
||||||
|
if 0 in accumulated_parameters:
|
||||||
|
params0 = json.loads(accumulated_parameters[0])
|
||||||
|
self.assertIn(
|
||||||
|
"北京",
|
||||||
|
accumulated_parameters[0],
|
||||||
|
"Should contain actual Chinese characters",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
params0["city"], "北京", "Should correctly parse first Chinese city"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check second tool call (上海 - Shanghai) if present
|
||||||
|
if 1 in accumulated_parameters:
|
||||||
|
params1 = json.loads(accumulated_parameters[1])
|
||||||
|
self.assertIn(
|
||||||
|
"上海",
|
||||||
|
accumulated_parameters[1],
|
||||||
|
"Should contain actual Chinese characters",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
params1["city"], "上海", "Should correctly parse second Chinese city"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestLlama32Detector(unittest.TestCase):
|
class TestLlama32Detector(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user