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