mirror of
https://github.com/hiyouga/LLaMA-Factory.git
synced 2026-09-06 23:35:46 +08:00
[data] add minicpm5 template with XML tool calling (#10801)
Co-authored-by: Caldalis <zyk_computer@163.com>
This commit is contained in:
@@ -315,7 +315,7 @@ Read technical notes:
|
||||
| [LLaVA-NeXT](https://huggingface.co/llava-hf) | 7B/8B/13B/34B/72B/110B | llava_next |
|
||||
| [LLaVA-NeXT-Video](https://huggingface.co/llava-hf) | 7B/34B | llava_next_video |
|
||||
| [MiMo](https://huggingface.co/XiaomiMiMo) | 7B/309B | mimo/mimo_v2 |
|
||||
| [MiniCPM 4/5](https://huggingface.co/openbmb) | 0.5B/1B/8B | cpm4/empty |
|
||||
| [MiniCPM 4/5](https://huggingface.co/openbmb) | 0.5B/1B/8B | cpm4/minicpm5 |
|
||||
| [MiniCPM-o/MiniCPM-V 4.5](https://huggingface.co/openbmb) | 8B/9B | minicpm_o/minicpm_v |
|
||||
| [MiniCPM-V 4.6](https://huggingface.co/openbmb) | 3B/8B | minicpm_v_4_6 |
|
||||
| [MiniMax-M1/MiniMax-M2](https://huggingface.co/MiniMaxAI/models) | 229B/456B | minimax1/minimax2 |
|
||||
|
||||
@@ -316,7 +316,7 @@ https://github.com/user-attachments/assets/43b700c6-a178-41db-b1f8-8190a5d3fcfc
|
||||
| [LLaVA-NeXT](https://huggingface.co/llava-hf) | 7B/8B/13B/34B/72B/110B | llava_next |
|
||||
| [LLaVA-NeXT-Video](https://huggingface.co/llava-hf) | 7B/34B | llava_next_video |
|
||||
| [MiMo](https://huggingface.co/XiaomiMiMo) | 7B/309B | mimo/mimo_v2 |
|
||||
| [MiniCPM 4/5](https://huggingface.co/openbmb) | 0.5B/1B/8B | cpm4/empty |
|
||||
| [MiniCPM 4/5](https://huggingface.co/openbmb) | 0.5B/1B/8B | cpm4/minicpm5 |
|
||||
| [MiniCPM-o/MiniCPM-V 4.5](https://huggingface.co/openbmb) | 8B/9B | minicpm_o/minicpm_v |
|
||||
| [MiniCPM-V 4.6](https://huggingface.co/openbmb) | 3B/8B | minicpm_v_4_6 |
|
||||
| [MiniMax-M1/MiniMax-M2](https://huggingface.co/MiniMaxAI/models) | 229B/456B | minimax1/minimax2 |
|
||||
|
||||
@@ -150,6 +150,12 @@ class Template:
|
||||
elements += self.format_prefix.apply()
|
||||
if system or tools:
|
||||
tool_text = self.format_tools.apply(content=tools)[0] if tools else ""
|
||||
if tools and not system:
|
||||
# Tool prompts that separate themselves from the system message with a
|
||||
# leading newline would otherwise emit a blank line when there is no
|
||||
# system message to separate from.
|
||||
tool_text = tool_text.lstrip("\n")
|
||||
|
||||
elements += self.format_system.apply(content=(system + tool_text))
|
||||
|
||||
if message["role"] == Role.USER:
|
||||
@@ -1823,6 +1829,23 @@ register_template(
|
||||
)
|
||||
|
||||
|
||||
register_template(
|
||||
name="minicpm5",
|
||||
format_user=StringFormatter(slots=["<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),
|
||||
format_assistant=StringFormatter(slots=["{{content}}<|im_end|>\n"]),
|
||||
format_system=StringFormatter(slots=["<|im_start|>system\n{{content}}<|im_end|>\n"]),
|
||||
format_function=FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5"),
|
||||
format_observation=StringFormatter(
|
||||
slots=["<|im_start|>user\n<tool_response>\n{{content}}\n</tool_response><|im_end|>\n<|im_start|>assistant\n"]
|
||||
),
|
||||
format_tools=ToolFormatter(tool_format="minicpm5"),
|
||||
format_prefix=EmptyFormatter(slots=[{"bos_token"}]),
|
||||
stop_words=["<|im_end|>"],
|
||||
replace_eos=True,
|
||||
template_class=ReasoningTemplate,
|
||||
)
|
||||
|
||||
|
||||
register_template(
|
||||
name="minimax1",
|
||||
format_user=StringFormatter(
|
||||
|
||||
@@ -119,6 +119,17 @@ LING_TOOL_PROMPT = (
|
||||
|
||||
LFM2_TOOL_PROMPT = "List of tools: <|tool_list_start|>{tool_text}<|tool_list_end|>"
|
||||
|
||||
MINICPM5_TOOL_PROMPT = (
|
||||
"\n\n# Tools\n\nYou are provided with function signatures within <tools></tools> XML tags:\n"
|
||||
"<tools>{tool_text}\n</tools>\n\nTool usage guidelines:\n"
|
||||
"- You may call zero or more functions. If no function calls are needed, just answer normally "
|
||||
"and do not include any <function ... </function>.\n"
|
||||
"- When calling a function, return an XML object within <function ... </function> using:\n"
|
||||
'<function name="function-name"><param name="param-name">param-value</param></function>\n'
|
||||
"- param-value may be multi-line. If it contains <, & or newline characters, wrap it in a "
|
||||
'CDATA block: <param name="param-name"><![CDATA[...multi-line value...]]></param>'
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolUtils(ABC):
|
||||
@@ -551,6 +562,72 @@ class MiniMaxM2ToolUtils(ToolUtils):
|
||||
return results
|
||||
|
||||
|
||||
class MiniCPM5ToolUtils(ToolUtils):
|
||||
r"""MiniCPM-5 tool using template."""
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def tool_formatter(tools: list[dict[str, Any]]) -> str:
|
||||
tool_text = ""
|
||||
for tool in tools:
|
||||
if tool.get("type") != "function":
|
||||
tool = {"type": "function", "function": tool}
|
||||
|
||||
tool_text += "\n" + json.dumps(tool, ensure_ascii=False)
|
||||
|
||||
return MINICPM5_TOOL_PROMPT.format(tool_text=tool_text)
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def function_formatter(functions: list["FunctionCall"]) -> str:
|
||||
function_texts = []
|
||||
for name, arguments in functions:
|
||||
prompt = f'<function name="{name}">'
|
||||
for key, value in json.loads(arguments).items():
|
||||
prompt += f'<param name="{key}">'
|
||||
if isinstance(value, str):
|
||||
if "<" in value or "&" in value or "\n" in value:
|
||||
prompt += f"<![CDATA[{value}]]>"
|
||||
else:
|
||||
prompt += value
|
||||
else:
|
||||
prompt += str(value)
|
||||
|
||||
prompt += "</param>"
|
||||
|
||||
prompt += "</function>"
|
||||
function_texts.append(prompt)
|
||||
|
||||
return "\n".join(function_texts)
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def tool_extractor(content: str) -> Union[str, list["FunctionCall"]]:
|
||||
results = []
|
||||
regex = re.compile(r'<function name="(.*?)">((?:<!\[CDATA\[.*?\]\]>|.)*?)</function>', re.DOTALL)
|
||||
for func_name, params_block in re.findall(regex, content):
|
||||
args_dict = {}
|
||||
param_pattern = re.compile(r'<param name="(.*?)">(<!\[CDATA\[.*?\]\]>|.*?)</param>', re.DOTALL)
|
||||
for key, raw_value in re.findall(param_pattern, params_block):
|
||||
cdata = re.fullmatch(r"<!\[CDATA\[(.*?)\]\]>", raw_value, re.DOTALL)
|
||||
if cdata:
|
||||
args_dict[key] = cdata.group(1)
|
||||
continue
|
||||
|
||||
value = raw_value.strip()
|
||||
try:
|
||||
args_dict[key] = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
args_dict[key] = ast.literal_eval(value)
|
||||
except Exception:
|
||||
args_dict[key] = raw_value
|
||||
|
||||
results.append(FunctionCall(func_name, json.dumps(args_dict, ensure_ascii=False)))
|
||||
|
||||
return results if results else content
|
||||
|
||||
|
||||
class MistralToolUtils(ToolUtils):
|
||||
r"""Mistral v0.3 tool using template."""
|
||||
|
||||
@@ -887,6 +964,7 @@ TOOLS = {
|
||||
"glm4": GLM4ToolUtils(),
|
||||
"llama3": Llama3ToolUtils(),
|
||||
"lfm2": LFM2ToolUtils(),
|
||||
"minicpm5": MiniCPM5ToolUtils(),
|
||||
"minimax1": MiniMaxM1ToolUtils(),
|
||||
"minimax2": MiniMaxM2ToolUtils(),
|
||||
"mistral": MistralToolUtils(),
|
||||
|
||||
@@ -1961,7 +1961,7 @@ register_model_group(
|
||||
DownloadSource.MODELSCOPE: "OpenBMB/MiniCPM5-1B",
|
||||
},
|
||||
},
|
||||
template="empty",
|
||||
template="minicpm5",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -380,3 +380,79 @@ def test_lfm2_tool_round_trip():
|
||||
assert len(extracted) == 1
|
||||
assert extracted[0][0] == original["name"]
|
||||
assert json.loads(extracted[0][1]) == original["arguments"]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
def test_minicpm5_function_formatter():
|
||||
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
|
||||
tool_calls = json.dumps(FUNCTION)
|
||||
assert formatter.apply(content=tool_calls) == [
|
||||
'<function name="tool_name"><param name="foo">bar</param><param name="size">10</param></function><|im_end|>\n'
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
def test_minicpm5_multi_function_formatter():
|
||||
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
|
||||
tool_calls = json.dumps([FUNCTION] * 2)
|
||||
assert formatter.apply(content=tool_calls) == [
|
||||
'<function name="tool_name"><param name="foo">bar</param>'
|
||||
'<param name="size">10</param></function>\n'
|
||||
'<function name="tool_name"><param name="foo">bar</param>'
|
||||
'<param name="size">10</param></function><|im_end|>\n'
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
def test_minicpm5_tool_formatter():
|
||||
formatter = ToolFormatter(tool_format="minicpm5")
|
||||
wrapped = json.dumps({"type": "function", "function": TOOLS[0]}, ensure_ascii=False)
|
||||
assert formatter.apply(content=json.dumps(TOOLS)) == [
|
||||
"\n\n# Tools\n\nYou are provided with function signatures within <tools></tools> XML tags:\n"
|
||||
f"<tools>\n{wrapped}\n</tools>\n\nTool usage guidelines:\n"
|
||||
"- You may call zero or more functions. If no function calls are needed, just answer "
|
||||
"normally and do not include any <function ... </function>.\n"
|
||||
"- When calling a function, return an XML object within <function ... </function> using:\n"
|
||||
'<function name="function-name"><param name="param-name">param-value</param></function>\n'
|
||||
"- param-value may be multi-line. If it contains <, & or newline characters, wrap it in a "
|
||||
'CDATA block: <param name="param-name"><![CDATA[...multi-line value...]]></param>'
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
def test_minicpm5_tool_extractor():
|
||||
formatter = ToolFormatter(tool_format="minicpm5")
|
||||
result = '<function name="test_tool"><param name="foo">bar</param><param name="size">10</param></function>'
|
||||
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
def test_minicpm5_tool_extractor_cdata():
|
||||
formatter = ToolFormatter(tool_format="minicpm5")
|
||||
result = '<function name="test_tool"><param name="foo"><![CDATA[a < b\nsecond line]]></param></function>'
|
||||
assert formatter.extract(result) == [("test_tool", json.dumps({"foo": "a < b\nsecond line"}))]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
def test_minicpm5_tool_extractor_malformed_value():
|
||||
formatter = ToolFormatter(tool_format="minicpm5")
|
||||
result = '<function name="test_tool"><param name="foo">{[1, 2], [3, 4]}</param></function>'
|
||||
assert formatter.extract(result) == [("test_tool", json.dumps({"foo": "{[1, 2], [3, 4]}"}))]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps"])
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
[
|
||||
{"foo": "x </param> y"},
|
||||
{"foo": "see </function> here"},
|
||||
{"foo": " padded "},
|
||||
{"foo": "a < b", "bar": "tom & jerry"},
|
||||
{"foo": True, "bar": [1, 2], "baz": {"k": "v"}},
|
||||
],
|
||||
)
|
||||
def test_minicpm5_tool_round_trip(arguments):
|
||||
formatter = ToolFormatter(tool_format="minicpm5")
|
||||
function_formatter = FunctionFormatter(slots=["{{content}}"], tool_format="minicpm5")
|
||||
rendered = function_formatter.apply(content=json.dumps({"name": "test_tool", "arguments": arguments}))[0]
|
||||
assert formatter.extract(rendered) == [("test_tool", json.dumps(arguments, ensure_ascii=False))]
|
||||
|
||||
Reference in New Issue
Block a user