Merge pull request #1365 from mr-emmett-one/fix/deepseek-multi-tool-calls-989
fix: support multiple parallel tool calls in DeepSeek V3 parser (#989)
This commit is contained in:
commit
15bf0b4af2
1 changed files with 28 additions and 15 deletions
|
|
@ -10,12 +10,13 @@ Format uses special unicode tokens:
|
||||||
<|tool▁call▁end|>
|
<|tool▁call▁end|>
|
||||||
<|tool▁calls▁end|>
|
<|tool▁calls▁end|>
|
||||||
|
|
||||||
Based on VLLM's DeepSeekV3ToolParser.extract_tool_calls()
|
Fixes Issue #989: Support for multiple simultaneous tool calls.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Optional
|
import logging
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from openai.types.chat.chat_completion_message_tool_call import (
|
from openai.types.chat.chat_completion_message_tool_call import (
|
||||||
ChatCompletionMessageToolCall,
|
ChatCompletionMessageToolCall,
|
||||||
|
|
@ -24,6 +25,7 @@ from openai.types.chat.chat_completion_message_tool_call import (
|
||||||
|
|
||||||
from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
|
from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@register_parser("deepseek_v3")
|
@register_parser("deepseek_v3")
|
||||||
class DeepSeekV3ToolCallParser(ToolCallParser):
|
class DeepSeekV3ToolCallParser(ToolCallParser):
|
||||||
|
|
@ -32,45 +34,56 @@ class DeepSeekV3ToolCallParser(ToolCallParser):
|
||||||
|
|
||||||
Uses special unicode tokens with fullwidth angle brackets and block elements.
|
Uses special unicode tokens with fullwidth angle brackets and block elements.
|
||||||
Extracts type, function name, and JSON arguments from the structured format.
|
Extracts type, function name, and JSON arguments from the structured format.
|
||||||
|
Ensures all tool calls are captured when the model executes multiple actions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
START_TOKEN = "<|tool▁calls▁begin|>"
|
START_TOKEN = "<|tool▁calls▁begin|>"
|
||||||
|
|
||||||
# Regex captures: type, function_name, function_arguments
|
# Updated PATTERN: Using \s* instead of literal \n for increased robustness
|
||||||
|
# against variations in model formatting (Issue #989).
|
||||||
PATTERN = re.compile(
|
PATTERN = re.compile(
|
||||||
r"<|tool▁call▁begin|>(?P<type>.*?)<|tool▁sep|>(?P<function_name>.*?)\n```json\n(?P<function_arguments>.*?)\n```<|tool▁call▁end|>",
|
r"<|tool▁call▁begin|>(?P<type>.*?)<|tool▁sep|>(?P<function_name>.*?)\s*```json\s*(?P<function_arguments>.*?)\s*```\s*<|tool▁call▁end|>",
|
||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, text: str) -> ParseResult:
|
def parse(self, text: str) -> ParseResult:
|
||||||
|
"""
|
||||||
|
Parses the input text and extracts all available tool calls.
|
||||||
|
"""
|
||||||
if self.START_TOKEN not in text:
|
if self.START_TOKEN not in text:
|
||||||
return text, None
|
return text, None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
matches = self.PATTERN.findall(text)
|
# Using finditer to capture ALL tool calls in the sequence
|
||||||
|
matches = list(self.PATTERN.finditer(text))
|
||||||
if not matches:
|
if not matches:
|
||||||
return text, None
|
return text, None
|
||||||
|
|
||||||
tool_calls: List[ChatCompletionMessageToolCall] = []
|
tool_calls: List[ChatCompletionMessageToolCall] = []
|
||||||
|
|
||||||
for match in matches:
|
for match in matches:
|
||||||
tc_type, func_name, func_args = match
|
func_name = match.group("function_name").strip()
|
||||||
|
func_args = match.group("function_arguments").strip()
|
||||||
|
|
||||||
tool_calls.append(
|
tool_calls.append(
|
||||||
ChatCompletionMessageToolCall(
|
ChatCompletionMessageToolCall(
|
||||||
id=f"call_{uuid.uuid4().hex[:8]}",
|
id=f"call_{uuid.uuid4().hex[:8]}",
|
||||||
type="function",
|
type="function",
|
||||||
function=Function(
|
function=Function(
|
||||||
name=func_name.strip(),
|
name=func_name,
|
||||||
arguments=func_args.strip(),
|
arguments=func_args,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not tool_calls:
|
if tool_calls:
|
||||||
return text, None
|
# Content is text before the first tool call block
|
||||||
|
content_index = text.find(self.START_TOKEN)
|
||||||
|
content = text[:content_index].strip()
|
||||||
|
return content if content else None, tool_calls
|
||||||
|
|
||||||
# Content is everything before the tool calls section
|
return text, None
|
||||||
content = text[: text.find(self.START_TOKEN)].strip()
|
|
||||||
return content if content else None, tool_calls
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing DeepSeek V3 tool calls: {e}")
|
||||||
except Exception:
|
|
||||||
return text, None
|
return text, None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue