[Tool Call] Stream DeepSeek-V3.2 function call parameters in JSON format. (#16091)
Co-authored-by: Huixxi <uestc.hugo@gmail.com>
This commit is contained in:
@@ -85,6 +85,7 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
r'<|DSML|invoke\s+name="([^"]+)"\s*>(.*?)(</|DSML|invoke>|$)'
|
||||
)
|
||||
self.prefix_parameter_end_call = ["</", "|DSML|", "parameter"]
|
||||
self.prefix_invoke_end_call = ["</", "|DSML|", "inv", "oke"]
|
||||
self.current_tool_id = -1
|
||||
|
||||
def has_tool_call(self, text: str) -> bool:
|
||||
@@ -93,9 +94,9 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
|
||||
def _parse_parameters_from_xml(
|
||||
self, invoke_content: str, allow_partial: bool = False
|
||||
) -> dict:
|
||||
) -> str:
|
||||
"""
|
||||
Parse parameters from either XML-like format or JSON format to dict.
|
||||
Parse parameters from either XML-like format or JSON format to str.
|
||||
|
||||
Supports two formats:
|
||||
1. XML parameter tags: <|DSML|parameter name="..." string="...">value</|DSML|parameter>
|
||||
@@ -103,17 +104,14 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
"""
|
||||
# First, try to parse as direct JSON (new format)
|
||||
invoke_content_stripped = invoke_content.strip()
|
||||
|
||||
if invoke_content_stripped.startswith("{") and invoke_content_stripped.endswith(
|
||||
"}"
|
||||
):
|
||||
try:
|
||||
parameters = json.loads(invoke_content_stripped)
|
||||
if isinstance(parameters, dict):
|
||||
return parameters
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# If JSON parsing fails, fall through to XML parsing
|
||||
pass
|
||||
if invoke_content_stripped.startswith("{"):
|
||||
if allow_partial:
|
||||
# Remove incomplete invoke end call prefix in case they are captured by param
|
||||
for token in reversed(self.prefix_invoke_end_call):
|
||||
invoke_content_stripped = invoke_content_stripped.rstrip(token)
|
||||
return invoke_content_stripped
|
||||
elif invoke_content_stripped.endswith("}"):
|
||||
return invoke_content_stripped
|
||||
|
||||
# Fall back to XML parameter tag parsing (original format)
|
||||
parameters = {}
|
||||
@@ -165,7 +163,7 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
except json.JSONDecodeError:
|
||||
parameters[param_name] = param_value.strip()
|
||||
|
||||
return parameters
|
||||
return json.dumps(parameters, ensure_ascii=False)
|
||||
|
||||
def detect_and_parse(self, text: str, tools: list[Tool]) -> StreamingParseResult:
|
||||
"""
|
||||
@@ -202,7 +200,7 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
# Parse parameters from XML format
|
||||
func_args = self._parse_parameters_from_xml(invoke_content)
|
||||
# construct match_result for parse_base_json
|
||||
match_result = {"name": func_name, "parameters": func_args}
|
||||
match_result = {"name": func_name, "parameters": json.loads(func_args)}
|
||||
calls.extend(self.parse_base_json(match_result, tools))
|
||||
|
||||
return StreamingParseResult(normal_text=normal_text, calls=calls)
|
||||
@@ -288,7 +286,6 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
current_params = self._parse_parameters_from_xml(
|
||||
invoke_content, allow_partial=not is_tool_end
|
||||
)
|
||||
current_args_json = json.dumps(current_params, ensure_ascii=False)
|
||||
|
||||
# 3. Calculate and send incremental arguments
|
||||
sent_len = len(self.streamed_args_for_tool[self.current_tool_id])
|
||||
@@ -300,12 +297,11 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
|
||||
if is_tool_end:
|
||||
# If complete, send everything remaining
|
||||
argument_diff = current_args_json[sent_len:]
|
||||
argument_diff = current_params[sent_len:]
|
||||
elif prev_params is not None:
|
||||
# If partial, send stable prefix diff
|
||||
prev_args_json = json.dumps(prev_params, ensure_ascii=False)
|
||||
if current_args_json != prev_args_json:
|
||||
prefix = _find_common_prefix(prev_args_json, current_args_json)
|
||||
if current_params != prev_params:
|
||||
prefix = _find_common_prefix(current_params, prev_params)
|
||||
if len(prefix) > sent_len:
|
||||
argument_diff = prefix[sent_len:]
|
||||
|
||||
@@ -353,5 +349,5 @@ class DeepSeekV32Detector(BaseFormatDetector):
|
||||
return lambda name: StructureInfo(
|
||||
begin=f'<|DSML|invoke name="{name}">',
|
||||
end="</|DSML|invoke>",
|
||||
trigger=f"<|DSML|invoke",
|
||||
trigger="<|DSML|invoke",
|
||||
)
|
||||
|
||||
@@ -1391,9 +1391,11 @@ class TestDeepSeekV32Detector(unittest.TestCase):
|
||||
|
||||
tool_calls_by_index = {}
|
||||
|
||||
num_tool_call_chunks = 0
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
for call in result.calls:
|
||||
num_tool_call_chunks += 1
|
||||
if call.tool_index is not None:
|
||||
if call.tool_index not in tool_calls_by_index:
|
||||
tool_calls_by_index[call.tool_index] = {
|
||||
@@ -1408,6 +1410,8 @@ class TestDeepSeekV32Detector(unittest.TestCase):
|
||||
"parameters"
|
||||
] += call.parameters
|
||||
|
||||
self.assertGreater(num_tool_call_chunks, 8)
|
||||
|
||||
self.assertEqual(len(tool_calls_by_index), 1)
|
||||
self.assertEqual(tool_calls_by_index[0]["name"], "get_favorite_tourist_spot")
|
||||
params = json.loads(tool_calls_by_index[0]["parameters"])
|
||||
@@ -1422,7 +1426,13 @@ class TestDeepSeekV32Detector(unittest.TestCase):
|
||||
text = """<|DSML|function_calls>
|
||||
<|DSML|invoke name="get_favorite_tourist_spot">
|
||||
{
|
||||
"city": "San Francisco"
|
||||
"city": "San Francisco",
|
||||
"another_city": "London",
|
||||
"topn": 10,
|
||||
"obj": {
|
||||
"name": "John",
|
||||
"age": 30
|
||||
}
|
||||
}
|
||||
</|DSML|invoke>
|
||||
</|DSML|function_calls>"""
|
||||
@@ -1436,9 +1446,11 @@ class TestDeepSeekV32Detector(unittest.TestCase):
|
||||
|
||||
tool_calls_by_index = {}
|
||||
|
||||
num_tool_call_chunks = 0
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
for call in result.calls:
|
||||
num_tool_call_chunks += 1
|
||||
if call.tool_index is not None:
|
||||
if call.tool_index not in tool_calls_by_index:
|
||||
tool_calls_by_index[call.tool_index] = {
|
||||
@@ -1453,6 +1465,7 @@ class TestDeepSeekV32Detector(unittest.TestCase):
|
||||
"parameters"
|
||||
] += call.parameters
|
||||
|
||||
self.assertGreater(num_tool_call_chunks, 8)
|
||||
self.assertEqual(len(tool_calls_by_index), 1)
|
||||
self.assertEqual(tool_calls_by_index[0]["name"], "get_favorite_tourist_spot")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user