212 lines
6.6 KiB
Python
212 lines
6.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""MiniMax M2 parser for XML-style tool calls.
|
|
|
|
MiniMax M2 tool call format::
|
|
|
|
<minimax:tool_call><invoke name="get_weather">
|
|
<parameter name="city">Seattle</parameter>
|
|
</invoke></minimax:tool_call>
|
|
|
|
Each ``<invoke>`` block becomes one tool call. The argument body consists
|
|
of ``<parameter name="...">...</parameter>`` tags.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import json
|
|
|
|
import regex as re
|
|
|
|
from vllm.parser.engine.events import EventType
|
|
from vllm.parser.engine.parser_engine import ParserEngine
|
|
from vllm.parser.engine.parser_engine_config import (
|
|
ParserEngineConfig,
|
|
ParserState,
|
|
Transition,
|
|
)
|
|
|
|
TOOL_CALL_START = "<minimax:tool_call>"
|
|
TOOL_CALL_END = "</minimax:tool_call>"
|
|
THINK_START = "<think>"
|
|
THINK_END = "</think>"
|
|
INVOKE_PREFIX_DQ = '<invoke name="'
|
|
INVOKE_PREFIX_SQ = "<invoke name='"
|
|
INVOKE_PREFIX_UNQUOTED = "<invoke name="
|
|
INVOKE_END = "</invoke>"
|
|
NAME_END_DQ = '">'
|
|
NAME_END_SQ = "'>"
|
|
NAME_END_UNQUOTED = ">"
|
|
PARAM_START = "<parameter name="
|
|
PARAM_END = "</parameter>"
|
|
_PARAM_RE = re.compile(
|
|
r"<\s*parameter\s+name\s*=\s*"
|
|
r"(?:\"(?P<dq_name>[^\"]*)\"|'(?P<sq_name>[^']*)'|(?P<bare_name>[^>\s]+))"
|
|
r"\s*>"
|
|
r"(?P<value>.*?)"
|
|
r"(?:<\s*/\s*parameter\s*>|(?=<\s*parameter\s+name\s*=))",
|
|
re.DOTALL,
|
|
)
|
|
_PARTIAL_PARAM_RE = re.compile(
|
|
r"<\s*parameter\s+name\s*=\s*"
|
|
r"(?:\"(?P<dq_name>[^\"]*)\"|'(?P<sq_name>[^']*)'|(?P<bare_name>[^>\s]+))"
|
|
r"\s*>"
|
|
r"(?P<value>.*)$",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def _minimax_m2_arg_converter(raw_args: str, partial: bool) -> str:
|
|
params: dict[str, object] = {}
|
|
|
|
for match in _PARAM_RE.finditer(raw_args):
|
|
name = (
|
|
match.group("dq_name")
|
|
or match.group("sq_name")
|
|
or match.group("bare_name")
|
|
or ""
|
|
).strip()
|
|
if not name:
|
|
continue
|
|
params[name] = match.group("value").strip()
|
|
|
|
if partial:
|
|
remaining = _PARAM_RE.sub("", raw_args)
|
|
match = _PARTIAL_PARAM_RE.search(remaining)
|
|
if match:
|
|
name = (
|
|
match.group("dq_name")
|
|
or match.group("sq_name")
|
|
or match.group("bare_name")
|
|
or ""
|
|
).strip()
|
|
if name:
|
|
params[name] = match.group("value").strip()
|
|
|
|
return json.dumps(params, ensure_ascii=False)
|
|
|
|
|
|
@functools.cache
|
|
def minimax_m2_config() -> ParserEngineConfig:
|
|
return ParserEngineConfig(
|
|
name="minimax_m2",
|
|
initial_state=ParserState.REASONING,
|
|
terminals={
|
|
"THINK_START": THINK_START,
|
|
"THINK_END": THINK_END,
|
|
"TOOL_START": TOOL_CALL_START,
|
|
"PARAM_START": PARAM_START,
|
|
"PARAM_END": PARAM_END,
|
|
"TOOL_END": TOOL_CALL_END,
|
|
"INVOKE_PREFIX_DQ": INVOKE_PREFIX_DQ,
|
|
"INVOKE_PREFIX_SQ": INVOKE_PREFIX_SQ,
|
|
"INVOKE_PREFIX_UNQUOTED": INVOKE_PREFIX_UNQUOTED,
|
|
"INVOKE_END": INVOKE_END,
|
|
"NAME_END_DQ": NAME_END_DQ,
|
|
"NAME_END_SQ": NAME_END_SQ,
|
|
"NAME_END_UNQUOTED": NAME_END_UNQUOTED,
|
|
},
|
|
token_id_terminals={
|
|
"THINK_START": THINK_START,
|
|
"THINK_END": THINK_END,
|
|
"TOOL_START": TOOL_CALL_START,
|
|
"TOOL_END": TOOL_CALL_END,
|
|
},
|
|
transitions={
|
|
(ParserState.REASONING, "THINK_START"): Transition(
|
|
ParserState.REASONING,
|
|
(),
|
|
),
|
|
(ParserState.REASONING, "THINK_END"): Transition(
|
|
ParserState.CONTENT,
|
|
(EventType.REASONING_END,),
|
|
),
|
|
(ParserState.CONTENT, "THINK_END"): Transition(
|
|
ParserState.CONTENT,
|
|
(),
|
|
),
|
|
(ParserState.REASONING, "TOOL_START"): Transition(
|
|
ParserState.TOOL_PREAMBLE,
|
|
(EventType.REASONING_END,),
|
|
),
|
|
(ParserState.CONTENT, "TOOL_START"): Transition(
|
|
ParserState.TOOL_PREAMBLE,
|
|
(),
|
|
),
|
|
(ParserState.TOOL_ARGS, "PARAM_START"): Transition(
|
|
ParserState.TOOL_ARGS,
|
|
(EventType.ARG_VALUE_CHUNK,),
|
|
),
|
|
(ParserState.TOOL_ARGS, "PARAM_END"): Transition(
|
|
ParserState.TOOL_ARGS,
|
|
(EventType.ARG_VALUE_CHUNK,),
|
|
),
|
|
(ParserState.TOOL_PREAMBLE, "TOOL_END"): Transition(
|
|
ParserState.CONTENT,
|
|
(),
|
|
),
|
|
(ParserState.TOOL_BETWEEN, "TOOL_END"): Transition(
|
|
ParserState.CONTENT,
|
|
(),
|
|
),
|
|
(ParserState.CONTENT, "TOOL_END"): Transition(
|
|
ParserState.CONTENT,
|
|
(),
|
|
),
|
|
(ParserState.TOOL_ARGS, "INVOKE_END"): Transition(
|
|
ParserState.TOOL_BETWEEN,
|
|
(EventType.TOOL_CALL_END,),
|
|
),
|
|
**{
|
|
(state, terminal): Transition(
|
|
ParserState.TOOL_NAME,
|
|
(EventType.TOOL_CALL_START,),
|
|
)
|
|
for state in (
|
|
ParserState.CONTENT,
|
|
ParserState.TOOL_PREAMBLE,
|
|
ParserState.TOOL_BETWEEN,
|
|
)
|
|
for terminal in (
|
|
"INVOKE_PREFIX_DQ",
|
|
"INVOKE_PREFIX_SQ",
|
|
"INVOKE_PREFIX_UNQUOTED",
|
|
)
|
|
},
|
|
**{
|
|
(ParserState.TOOL_NAME, terminal): Transition(
|
|
ParserState.TOOL_ARGS,
|
|
(),
|
|
)
|
|
for terminal in (
|
|
"NAME_END_DQ",
|
|
"NAME_END_SQ",
|
|
"NAME_END_UNQUOTED",
|
|
)
|
|
},
|
|
},
|
|
arg_converter=_minimax_m2_arg_converter,
|
|
stream_arg_deltas=True,
|
|
tool_args_json=False,
|
|
validate_tool_names=True,
|
|
)
|
|
|
|
|
|
class MinimaxM2Parser(ParserEngine):
|
|
"""MiniMax M2 parser backed by the declarative parser engine."""
|
|
|
|
def __init__(self, tokenizer, tools=None, **kwargs) -> None:
|
|
kwargs.setdefault("parser_engine_config", minimax_m2_config())
|
|
super().__init__(tokenizer, tools, **kwargs)
|
|
self._think_end_token_id = self.vocab.get(THINK_END)
|
|
|
|
def extract_content_ids(self, input_ids: list[int]) -> list[int]:
|
|
end_id = self._think_end_token_id
|
|
if end_id is None:
|
|
return []
|
|
for i in range(len(input_ids) - 1, -1, -1):
|
|
if input_ids[i] == end_id:
|
|
return input_ids[i + 1 :]
|
|
return []
|