chore: import upstream snapshot with attribution
Lint test / lint (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:58 +08:00
commit a203934033
1368 changed files with 175001 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
from .base import BaseAgentTemplate
from .mapping import agent_template_map
+248
View File
@@ -0,0 +1,248 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
"""
Agent template module for handling tool calling and function execution.
This module provides base classes and utilities for creating agent templates
that support tool calling in conversational AI systems. It includes support
for various agent formats like ReAct, function calling, and parallel execution.
"""
import ast
import json
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt, split_str_parts_by
@dataclass
class AgentKeyword:
action: str = 'Action:'
action_input: str = 'Action Input:'
observation: str = 'Observation:'
@dataclass
class ToolDesc:
name_for_model: str
name_for_human: str
description_for_model: str
parameters: str
args_format: str
class ReactCompatMixin:
"""
Mixin class providing ReAct-style agent compatibility.
This mixin handles parsing and formatting of tool calls in the ReAct format,
where actions and inputs are marked with specific keywords in the text.
"""
keyword = AgentKeyword()
@staticmethod
def _split_action_action_input(response: str, keyword: AgentKeyword) -> List[Function]:
agent_parts = split_str_parts_by(response, list(asdict(keyword).values()))
functions = []
action_content = None
for part in agent_parts:
key, content = part['key'].lower(), part['content']
if action_content is None and key == keyword.action.lower():
action_content = content
elif action_content is not None and key == keyword.action_input.lower():
functions.append(Function(name=action_content, arguments=content))
action_content = None
return functions
def get_toolcall(self, response: str) -> List[Function]:
"""
Extract tool calls from an agent response.
Args:
response: The agent's response text.
Returns:
List of Function objects representing tool calls.
"""
functions = self._split_action_action_input(response, self.keyword)
if len(functions) == 0 and self.keyword != ReactCompatMixin.keyword:
# compat react
functions = self._split_action_action_input(response, ReactCompatMixin.keyword)
return functions
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
"""
Format tool execution results into the conversation.
Args:
assistant_content: The assistant's message containing tool calls.
tool_messages: List of tool execution result messages.
Returns:
Tuple of (formatted assistant content, formatted tool responses).
"""
assert len(tool_messages) > 0
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
if not assistant_content.endswith(self.keyword.observation):
if not assistant_content.endswith('\n'):
assistant_content += '\n'
assistant_content += self.keyword.observation
res = []
for i, tool_message in enumerate(tool_messages):
if i > 0:
res.append(self.keyword.observation)
tool_content = tool_message['content']
res.append(tool_content)
if not tool_content.endswith('\n'):
res.append('\n')
else:
res = []
for tool_message in tool_messages:
res.append(tool_message['content'])
return assistant_content, res
@staticmethod
def _parse_tool_call(content) -> Dict[str, Any]:
obj = BaseAgentTemplate._parse_json(content)
name = obj['name']
arguments = obj.get('arguments')
if arguments is None:
arguments = obj.get('parameters')
arguments = BaseAgentTemplate._parse_json(arguments)
assert arguments is not None, f'content: {content}'
return {'name': name, 'arguments': arguments}
def _format_tool_calls(self, tool_call_messages) -> str:
"""
Format tool call messages into ReAct format.
Args:
tool_call_messages: List of messages containing tool call information.
Returns:
Formatted string with Action, Action Input, and Observation markers.
"""
# -> assistant_content
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
tool_calls.append(f'{self.keyword.action} {tool_call["name"]}\n'
f'{self.keyword.action_input} {tool_call["arguments"]}\n')
tool_calls.append(self.keyword.observation)
return ''.join(tool_calls)
class BaseAgentTemplate(ReactCompatMixin, ABC):
"""
Abstract base class for agent templates.
This class provides common functionality for parsing and formatting tools,
as well as handling tool calls in different formats. Subclasses must
implement the following methods to define their specific behavior:
- `_format_tools`: Format tool definitions for the prompt
- `_format_tool_calls`: Format tool call messages
- `_format_tool_responses`: Format tool execution results
- `get_toolcall`: Extract tool calls from agent responses
"""
def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
"""Hook to prepend a separator before tool_call content based on the preceding message.
Subclasses can override this to match their jinja template's separator logic
(e.g., Qwen3.5/3.6 inserts '\n\n' when assistant has effective content before tool_calls).
Args:
tool_content: The formatted tool_call string from _format_tool_calls.
pre_message: The message immediately before the tool_call block, or None.
Returns:
tool_content with any necessary prefix prepended.
"""
return tool_content
@staticmethod
def _get_tool_name(tool):
return tool.get('name_for_model') or tool.get('name')
@staticmethod
def unwrap_tool(tool):
assert isinstance(tool, dict), f'tool: {tool}'
if 'type' in tool and 'function' in tool:
tool = tool['function']
return tool
@staticmethod
def wrap_tool(tool):
assert isinstance(tool, dict), f'tool: {tool}'
if 'type' not in tool and 'function' not in tool:
tool = {'type': 'function', 'function': tool}
return tool
@staticmethod
def _parse_tool(tool, lang: Literal['zh', 'en']) -> ToolDesc:
tool = BaseAgentTemplate.unwrap_tool(tool)
name_for_model = BaseAgentTemplate._get_tool_name(tool)
name_for_human = tool.get('name_for_human') or name_for_model
description = tool.get('description')
if description is None:
description = tool.get('description_for_model')
parameters = tool.get('parameters') or {}
parameters = parameters if isinstance(parameters, str) else json.dumps(parameters, ensure_ascii=False)
args_format = '此工具的输入应为JSON对象。' if lang == 'zh' else 'Format the arguments as a JSON object.'
tool_desc = ToolDesc(
name_for_model=name_for_model,
name_for_human=name_for_human,
description_for_model=description,
parameters=parameters,
args_format=args_format)
assert name_for_model is not None and description is not None, f'tool_desc: {tool_desc}'
return tool_desc
@staticmethod
def _parse_json(json_str: str) -> Optional[Any]:
"""
Parse a JSON string with fallback to ast.literal_eval.
Args:
json_str: String to parse, or already parsed object.
Returns:
Parsed object, or None if parsing fails.
"""
if not isinstance(json_str, str):
return json_str
try:
res = json.loads(json_str)
except json.JSONDecodeError:
try:
res = ast.literal_eval(json_str)
except Exception:
return
return res
@abstractmethod
def _format_tools(self,
tools: List[Union[str, dict]],
system: Optional[str] = None,
user_message: Optional[dict] = None) -> str:
"""
Format tools for inclusion in the agent prompt.
Args:
tools: List of tool definitions (strings or dictionaries).
system: System prompt text.
user_message: Optional user message to incorporate.
Returns:
Formatted string to include in the prompt.
"""
pass
+87
View File
@@ -0,0 +1,87 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class DeepSeekV31AgentTemplate(BaseAgentTemplate):
def get_toolcall(self, response: str) -> List[Function]:
# Parse tool calls using the DSV3.1 format:
# <tool▁calls▁begin><tool▁call▁begin>name<tool▁sep>args<tool▁call▁end>
pattern = r'<tool▁call▁begin>(.*?)<tool▁sep>(.*?)<tool▁call▁end>'
res_list = re.findall(pattern, response, re.DOTALL)
functions = []
for name, arguments in res_list:
name = name.strip()
arguments = self._parse_json(arguments.strip())
if arguments is not None:
functions.append(Function(name=name, arguments=arguments))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _get_tool_responses(self, tool_messages):
return ''.join(f'<tool▁output▁begin>{tool_message["content"]}<tool▁output▁end>'
for tool_message in tool_messages)
def _get_tool_calls(self, tool_calls: List[str]):
return f'<tool▁calls▁begin>{"".join(tool_calls)}<tool▁calls▁end>'
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
res = ['<end▁of▁sentence>', self._get_tool_responses(tool_messages)]
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = []
system = system or ''
for tool in tools:
tool = self.unwrap_tool(tool)
tool_name = self._get_tool_name(tool)
description = tool.get('description', '')
parameters = tool.get('parameters', {})
tool_desc = f"""### {tool_name}
Description: {description}
Parameters: {json.dumps(parameters, ensure_ascii=False)}"""
tool_descs.append(tool_desc)
tools_section = '\n\n'.join(tool_descs)
return f"""{system}
## Tools
You have access to the following tools:
{tools_section}
IMPORTANT: ALWAYS adhere to this exact format for tool use:
<tool▁calls▁begin><tool▁call▁begin>tool_call_name<tool▁sep>tool_call_arguments<tool▁call▁end>{{additional_tool_calls}}<tool▁calls▁end>
Where:
- `tool_call_name` must be an exact match to one of the available tools
- `tool_call_arguments` must be valid JSON that strictly follows the tool's Parameters Schema
- For multiple tool calls, chain them directly without separators or spaces"""
def _format_tool_calls(self, tool_call_messages):
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = json.dumps(tool_call['arguments'], ensure_ascii=False)
tool_calls.append(f'<tool▁call▁begin>{name}<tool▁sep>{arguments}<tool▁call▁end>')
return self._get_tool_calls(tool_calls)
+138
View File
@@ -0,0 +1,138 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import Any, Dict, List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
DSML_TOKEN = 'DSML'
TOOLS_TEMPLATE = """## Tools
You have access to a set of tools to help answer the user's question. \
You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
<{dsml_token}tool_calls>
<{dsml_token}invoke name="$TOOL_NAME">
<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
...
</{dsml_token}invoke>
<{dsml_token}invoke name="$TOOL_NAME2">
...
</{dsml_token}invoke>
</{dsml_token}tool_calls>
String parameters should be specified as is and set `string="true"`. \
For all other types (numbers, booleans, arrays, objects), \
pass the value in JSON format and set `string="false"`.
If thinking_mode is enabled (triggered by <think>), \
you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
Otherwise, output directly after </think> with tool calls or final response.
### Available Tool Schemas
{tool_schemas}
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
"""
def _to_json(value: Any) -> str:
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return json.dumps(value, ensure_ascii=True)
def _encode_arguments_to_dsml(arguments: Dict[str, Any]) -> str:
"""Encode tool call arguments dict into DSML parameter lines."""
lines = []
for k, v in arguments.items():
is_str = 'true' if isinstance(v, str) else 'false'
val = v if isinstance(v, str) else _to_json(v)
lines.append(f'<{DSML_TOKEN}parameter name="{k}" string="{is_str}">{val}</{DSML_TOKEN}parameter>')
return '\n'.join(lines)
class DeepSeekV4AgentTemplate(BaseAgentTemplate):
def get_toolcall(self, response: str) -> List[Function]:
# Parse DSML tool calls from model output
# Pattern: <DSMLinvoke name="tool_name">...params...</DSMLinvoke>
invoke_pattern = re.compile(
rf'<{re.escape(DSML_TOKEN)}invoke\s+name="([^"]+)">\s*(.*?)\s*</{re.escape(DSML_TOKEN)}invoke>', re.DOTALL)
param_pattern = re.compile(
rf'<{re.escape(DSML_TOKEN)}parameter\s+name="([^"]+)"\s+string="(true|false)">'
rf'(.*?)</{re.escape(DSML_TOKEN)}parameter>', re.DOTALL)
functions = []
for match in invoke_pattern.finditer(response):
tool_name = match.group(1)
params_block = match.group(2)
arguments = {}
for pm in param_pattern.finditer(params_block):
param_name = pm.group(1)
is_string = pm.group(2)
param_value = pm.group(3)
if is_string == 'false':
try:
param_value = json.loads(param_value)
except json.JSONDecodeError:
pass
arguments[param_name] = param_value
functions.append(Function(name=tool_name, arguments=json.dumps(arguments, ensure_ascii=False)))
if len(functions) == 0:
# Fallback to ReAct format
return super().get_toolcall(response)
return functions
def _get_tool_responses(self, tool_messages):
return ''.join(f'<tool_result>{tool_message["content"]}</tool_result>' for tool_message in tool_messages)
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
res = [
'<end▁of▁sentence><User>',
self._get_tool_responses(tool_messages),
'<Assistant>',
]
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_schemas = []
for tool in tools:
tool = self.unwrap_tool(tool)
tool_schemas.append(_to_json(tool))
tools_section = TOOLS_TEMPLATE.format(
tool_schemas='\n'.join(tool_schemas),
dsml_token=DSML_TOKEN,
)
system = system or ''
return f'{system}\n\n{tools_section}' if system else tools_section
def _format_tool_calls(self, tool_call_messages) -> str:
invocations = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = tool_call['arguments']
if isinstance(arguments, str):
arguments = json.loads(arguments)
dsml_args = _encode_arguments_to_dsml(arguments)
invocations.append(f'<{DSML_TOKEN}invoke name="{name}">\n{dsml_args}\n</{DSML_TOKEN}invoke>')
tool_calls_str = '\n'.join(invocations)
return f'<{DSML_TOKEN}tool_calls>\n{tool_calls_str}\n</{DSML_TOKEN}tool_calls>'
+36
View File
@@ -0,0 +1,36 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
from typing import List, Optional, Union
from .base import BaseAgentTemplate
class ReactGRPOAgentTemplate(BaseAgentTemplate):
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names = []
tool_descs = []
for tool in tools:
tool_desc = self._parse_tool(tool, 'en')
tool_names.append(tool_desc.name_for_model)
tool_descs.append(
f'{tool_desc.name_for_model}: Call this tool to interact with the {tool_desc.name_for_human} API. '
f'What is the {tool_desc.name_for_human} API useful for? {tool_desc.description_for_model} '
f'Parameters: {tool_desc.parameters} {tool_desc.args_format}')
return """A conversation for tool calling between User and Assistant. The user asks a question which may be solved by calling tools, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process should be enclosed within <think> </think>tags and answer should follow the ReACT format(Action:xxx\nAction Input:xxx), i.e., <think> reasoning process here </think> Action: action here\nAction Input: parameters here
Answer the following questions as best as you can. You have access to the following tools:
""" + '\n\n'.join(tool_descs) + f"""
Use the following format:
<think>you should always think about what to do</think>
Action: the action to take, should be one of [{','.join(tool_names)}]
Action Input: the input to the action
Observation: the result of the action, given by the actual calling
... (this Thought/Action/Action Input/Observation can be repeated zero or more times)
Final Answer: the final answer to the original input question
Begin!
""" # noqa
+210
View File
@@ -0,0 +1,210 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import Any, Dict, List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
QUOTE = '<|"|>'
_STANDARD_KEYS = {'description', 'type', 'properties', 'required', 'nullable'}
class Gemma4AgentTemplate(BaseAgentTemplate):
"""Agent template for Google Gemma-4 models.
Reference: chat_template.jinja shipped with google/gemma-4-12B-it.
Tool definitions are wrapped in `<|tool>...<tool|>` and rendered with the
custom DSL described by the official chat template.
Tool calls follow `<|tool_call>call:NAME{key:value,...}<tool_call|>` and
tool responses follow `<|tool_response>response:NAME{...}<tool_response|>`.
"""
@classmethod
def _format_argument(cls, value: Any, escape_keys: bool = True) -> str:
if isinstance(value, bool):
return 'true' if value else 'false'
if isinstance(value, str):
return f'{QUOTE}{value}{QUOTE}'
if value is None:
return 'null'
if isinstance(value, dict):
items = []
for k in sorted(value.keys()):
v = value[k]
key_str = f'{QUOTE}{k}{QUOTE}' if escape_keys else str(k)
items.append(f'{key_str}:{cls._format_argument(v, escape_keys=escape_keys)}')
return '{' + ','.join(items) + '}'
if isinstance(value, (list, tuple)):
return '[' + ','.join(cls._format_argument(item, escape_keys=escape_keys) for item in value) + ']'
return str(value)
@classmethod
def _format_parameters(cls,
properties: Dict[str, Any],
required: Optional[List[str]] = None,
filter_keys: bool = False) -> str:
parts = []
for key in sorted(properties.keys()):
value = properties[key]
if filter_keys and key in _STANDARD_KEYS:
continue
if not isinstance(value, dict):
continue
inner: List[str] = []
type_upper = (value.get('type') or '').upper() if isinstance(value.get('type'), str) else ''
if value.get('description'):
inner.append(f'description:{QUOTE}{value["description"]}{QUOTE}')
if type_upper == 'STRING':
if value.get('enum'):
inner.append(f'enum:{cls._format_argument(value["enum"])}')
elif type_upper == 'ARRAY':
items_value = value.get('items')
if isinstance(items_value, dict) and items_value:
items_inner: List[str] = []
items_required = items_value.get('required', [])
for item_key in sorted(items_value.keys()):
item_value = items_value[item_key]
if item_value is None:
continue
if item_key == 'properties' and isinstance(item_value, dict):
items_inner.append(f'properties:{{{cls._format_parameters(item_value, items_required)}}}')
elif item_key == 'required':
req_str = ','.join(f'{QUOTE}{r}{QUOTE}' for r in item_value)
items_inner.append(f'required:[{req_str}]')
elif item_key == 'type':
if isinstance(item_value, str):
items_inner.append(f'type:{cls._format_argument(item_value.upper())}')
else:
items_inner.append(f'type:{cls._format_argument([str(t).upper() for t in item_value])}')
else:
items_inner.append(f'{item_key}:{cls._format_argument(item_value)}')
inner.append('items:{' + ','.join(items_inner) + '}')
if value.get('nullable'):
inner.append('nullable:true')
if type_upper == 'OBJECT':
inner_required = value.get('required', [])
if isinstance(value.get('properties'), dict):
inner.append(f'properties:{{{cls._format_parameters(value["properties"], inner_required)}}}')
else:
inner.append(f'properties:{{{cls._format_parameters(value, inner_required, filter_keys=True)}}}')
if value.get('required'):
req_str = ','.join(f'{QUOTE}{r}{QUOTE}' for r in value['required'])
inner.append(f'required:[{req_str}]')
inner.append(f'type:{QUOTE}{type_upper}{QUOTE}')
parts.append(f'{key}:{{{",".join(inner)}}}')
return ','.join(parts)
@classmethod
def _format_function_declaration(cls, tool: Dict[str, Any]) -> str:
function = tool['function']
name = function.get('name', '')
description = function.get('description', '') or ''
result = f'declaration:{name}{{description:{QUOTE}{description}{QUOTE}'
params = function.get('parameters')
if params:
param_parts: List[str] = []
properties = params.get('properties')
if properties:
param_parts.append(f'properties:{{{cls._format_parameters(properties, params.get("required", []))}}}')
if params.get('required'):
req_str = ','.join(f'{QUOTE}{r}{QUOTE}' for r in params['required'])
param_parts.append(f'required:[{req_str}]')
ptype = params.get('type')
if isinstance(ptype, str) and ptype:
param_parts.append(f'type:{QUOTE}{ptype.upper()}{QUOTE}')
if param_parts:
result += ',parameters:{' + ','.join(param_parts) + '}'
result += '}'
return result
def _format_tools(self,
tools: List[Union[str, dict]],
system: Optional[str] = None,
user_message: Optional[dict] = None) -> str:
tool_blocks: List[str] = []
for tool in tools:
tool = self.wrap_tool(tool)
tool_blocks.append(f'<|tool>{self._format_function_declaration(tool)}<tool|>')
system_text = (system or '').strip()
return system_text + ''.join(tool_blocks)
def _format_tool_calls(self, tool_call_messages) -> str:
invocations: List[str] = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = tool_call['arguments']
if isinstance(arguments, str):
arguments = self._parse_json(arguments) or {}
if isinstance(arguments, dict):
args_str = ','.join(f'{k}:{self._format_argument(arguments[k], escape_keys=False)}'
for k in sorted(arguments.keys()))
else:
args_str = ''
invocations.append(f'<|tool_call>call:{name}{{{args_str}}}<tool_call|>')
return ''.join(invocations)
def _get_tool_responses(self, tool_messages) -> str:
parts: List[str] = []
for tool_message in tool_messages:
tool_name = tool_message.get('name') or 'unknown'
tool_content = tool_message.get('content')
if isinstance(tool_content, dict):
inner = ','.join(f'{k}:{self._format_argument(tool_content[k], escape_keys=False)}'
for k in sorted(tool_content.keys()))
parts.append(f'<|tool_response>response:{tool_name}{{{inner}}}<tool_response|>')
else:
# Match jinja: treat string/other content as a single `value:` field.
value = '' if tool_content is None else tool_content
parts.append(f'<|tool_response>response:{tool_name}'
f'{{value:{self._format_argument(value, escape_keys=False)}}}<tool_response|>')
return ''.join(parts)
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
# If the model hallucinated a trailing `<|tool_response>` opener (e.g. when stop
# tokens were not configured), strip it so the rendered turn does not contain
# `<|tool_response><|tool_response>response:...`.
if assistant_content.endswith('<|tool_response>'):
assistant_content = assistant_content[:-len('<|tool_response>')]
# In gemma4, tool_call/tool_response/follow-up assistant text all live in the
# same `<|turn>model ... <turn|>` block, so we do not open a new model turn here.
res: 'Prompt' = [self._get_tool_responses(tool_messages)]
return assistant_content, res
@classmethod
def _gemma_to_json(cls, s: str) -> str:
# `<|"|>` -> `"`; bare keys preceded by `{` or `,` get JSON-quoted.
s = s.replace(QUOTE, '"')
s = re.sub(r'(?<=[\{,])([A-Za-z_][\w\-]*)(?=:)', r'"\1"', s)
return s
@classmethod
def _parse_arguments(cls, args_body: str) -> Dict[str, Any]:
json_str = cls._gemma_to_json('{' + args_body + '}')
try:
parsed = json.loads(json_str)
if isinstance(parsed, dict):
return parsed
except json.JSONDecodeError:
pass
return {}
def get_toolcall(self, response: str) -> List[Function]:
pattern = re.compile(r'<\|tool_call>call:([^\{]+)\{(.*?)\}<tool_call\|>', re.DOTALL)
functions: List[Function] = []
for match in pattern.finditer(response):
name = match.group(1).strip()
arguments = self._parse_arguments(match.group(2))
functions.append(Function(name=name, arguments=json.dumps(arguments, ensure_ascii=False)))
if not functions:
return super().get_toolcall(response)
return functions
+178
View File
@@ -0,0 +1,178 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class ChatGLM4AgentTemplate(BaseAgentTemplate):
is_glm4_0414 = False
@staticmethod
def _find_function_call(single_content: str) -> Optional[Function]:
single_content = single_content.replace('<|observation|>', '')
pattern = re.compile(r'([^\n`]*?)\n({.*?})(?=\w*\n|$)', re.DOTALL)
matches = pattern.findall(single_content)
if not matches:
return
name, arguments = matches[0]
return Function(name=name, arguments=arguments)
def get_toolcall(self, response: str) -> List[Function]:
toolcall_list = response.split('<|assistant|>')
functions = []
for toolcall in toolcall_list:
function = self._find_function_call(toolcall)
if function:
functions.append(function)
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = []
for tool in tools:
tool = self.unwrap_tool(tool)
name = self._get_tool_name(tool)
tool_descs.append(f'## {name}\n\n{json.dumps(tool, ensure_ascii=False, indent=4)}\n'
'在调用上述函数时,请使用 Json 格式表示调用的参数。')
glm4_system = '你是一个名为 GLM-4 的人工智能助手。你是基于智谱AI训练的语言模型 GLM-4 模型开发的,你的任务是针对用户的问题和要求提供适当的答复和支持。\n\n' # noqa
return ('' if self.is_glm4_0414 else glm4_system) + """# 可用工具
""" + '\n'.join(tool_descs)
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
res = ['\n']
for i, tool_message in enumerate(tool_messages):
tool_content = tool_message['content']
if i > 0:
res.append('<|observation|>\n')
res.append(tool_content)
res.append('<|assistant|>\n')
return assistant_content, res
def _format_tool_calls(self, tool_call_messages) -> str:
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
tool_calls.append(f'{tool_call["name"]}\n{tool_call["arguments"]}')
return '<|assistant|>'.join(tool_calls) + '<|observation|>'
class GLM4AgentTemplate(ChatGLM4AgentTemplate):
is_glm4_0414 = True
class GLM4_5AgentTemplate(BaseAgentTemplate):
model_type = 'glm4_5'
@staticmethod
def _find_function_call(single_content: str) -> Optional[Function]:
single_content = single_content.strip()
func_name_match = re.match(r'^([^\n<]+)', single_content)
if not func_name_match:
return None
func_name = func_name_match.group(1).strip()
keys = re.findall(r'<arg_key>(.*?)</arg_key>', single_content, re.DOTALL)
values = re.findall(r'<arg_value>(.*?)</arg_value>', single_content, re.DOTALL)
if len(keys) != len(values):
return None
args = {k.strip(): v.strip() for k, v in zip(keys, values)}
return Function(name=func_name, arguments=json.dumps(args, ensure_ascii=False))
def get_toolcall(self, response: str) -> List[Function]:
toolcall_list = re.findall(r'<tool_call>(.*?)</tool_call>', response, re.DOTALL)
functions = []
for toolcall in toolcall_list:
function = self._find_function_call(toolcall)
if function:
functions.append(function)
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [
'# Tools\n\nYou may call one or more functions to assist with the user query.\n\n'
'You are provided with function signatures within <tools></tools> XML tags:\n<tools>'
]
for tool in tools:
if self.model_type == 'glm5_1':
tool = self.unwrap_tool(tool)
tool_descs.append(f'{json.dumps(tool, ensure_ascii=False)}')
if self.model_type == 'glm4_5':
tool_desc = ('</tools>\n\nFor each function call, output the function name and arguments within '
'the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n'
'<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n'
'<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>')
elif self.model_type in {'glm4_7', 'glm5_1'}:
tool_desc = ('</tools>\n\nFor each function call, output the function name and arguments within '
'the following XML format:\n<tool_call>{function-name}<arg_key>{arg-key-1}</arg_key>'
'<arg_value>{arg-value-1}</arg_value><arg_key>{arg-key-2}</arg_key><arg_value>'
'{arg-value-2}</arg_value>...</tool_call>')
else:
raise ValueError("model_type must be one of 'glm4_5', 'glm4_7', or 'glm5_1'.")
tool_descs.append(tool_desc)
tool_descs = '\n'.join(tool_descs)
if system is not None and system.strip():
tool_descs += '<|system|>\n' + system.strip()
elif self.model_type in {'glm4_7', 'glm5_1'} and not tool_descs.startswith('\n'):
tool_descs = '\n' + tool_descs
return tool_descs
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
if self.model_type == 'glm4_5':
res = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res.append(f'\n<tool_response>\n{tool_content}\n</tool_response>')
res.append('<|assistant|>\n')
elif self.model_type in {'glm4_7', 'glm5_1'}:
res = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res.append(f'<tool_response>{tool_content}</tool_response>')
res.append('<|assistant|>')
return assistant_content, res
def _format_tool_calls(self, tool_call_messages) -> str:
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
tool_calls.append(f"<tool_call>{tool_call['name']}")
for arg_key, arg_value in tool_call['arguments'].items():
tool_calls.append(f'<arg_key>{arg_key}</arg_key>')
tool_calls.append(f'<arg_value>{arg_value}</arg_value>')
tool_calls.append('</tool_call>')
if self.model_type == 'glm4_5':
sep = '\n'
elif self.model_type in {'glm4_7', 'glm5_1'}:
sep = ''
return sep.join(tool_calls) + '<|observation|>'
class GLM4_7AgentTemplate(GLM4_5AgentTemplate):
model_type = 'glm4_7'
class GLM5_1AgentTemplate(GLM4_5AgentTemplate):
model_type = 'glm5_1'
+127
View File
@@ -0,0 +1,127 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class HermesAgentTemplate(BaseAgentTemplate):
def get_toolcall(self, response: str) -> List[Function]:
res_list = re.findall(r'<tool_call>(.+?)</tool_call>', response, re.DOTALL)
functions = []
for res in res_list:
res = self._parse_json(res)
if isinstance(res, dict) and 'name' in res and 'arguments' in res:
functions.append(Function(name=res['name'], arguments=res['arguments']))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response>\n{tool_content}\n</tool_response>')
return '\n'.join(res_tool)
def _get_tool_calls(self, tool_calls: List[str]):
return '\n'.join(tool_calls)
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
if hasattr(self, 'template_meta'):
prompt = self.template_meta.prompt
chat_sep = self.template_meta.chat_sep
else:
prompt = ['<|im_start|>user\n{{QUERY}}<|im_end|>\n<|im_start|>assistant\n']
chat_sep = ['<|im_end|>\n']
res = chat_sep.copy()
total_tool = self._get_tool_responses(tool_messages)
for context in prompt:
if isinstance(context, str):
context = context.replace('{{QUERY}}', total_tool)
res.append(context)
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
system = system or ''
return f"""{system}
# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
""" + '\n'.join(tool_descs) + """
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call>"""
def _format_tool_calls(self, tool_call_messages):
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
tool_calls.append(f'<tool_call>\n{json.dumps(tool_call, ensure_ascii=False)}\n</tool_call>')
return self._get_tool_calls(tool_calls)
class HunyuanHermesAgentTemplate(HermesAgentTemplate):
def get_toolcall(self, response: str) -> List[Function]:
res_list = re.findall(r'<tool_call>(.+?)\n```json(.+?)```</tool_call>', response, re.DOTALL)
functions = []
for name, arguments in res_list:
arguments = self._parse_json(arguments)
functions.append(Function(name=name, arguments=arguments))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response>{tool_content}</tool_response>')
tool_responses = '\n'.join(res_tool)
return f'<tool_responses>{tool_responses}</tool_responses>'
def _get_tool_calls(self, tool_calls: List[str]):
tool_calls = '\n'.join(tool_calls)
return f'<tool_calls>\n{tool_calls}\n</tool_calls>'
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
system = system or ''
if system:
system = f'{system}\n\n'
return f"""{system}# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
""" + '\n'.join(tool_descs) + """
</tools>
For function call returns, you should first print <tool_calls>For each function call, you should return object like:
<tool_call>function_name
```json
function_arguments_in_json_format
```</tool_call>At the end of function call returns, you should print </tool_calls>"""
+107
View File
@@ -0,0 +1,107 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class HyV3PreviewAgentTemplate(BaseAgentTemplate):
HYTK = ''
def get_toolcall(self, response: str) -> List[Function]:
# Parse tool calls from <tool_calls>...<tool_call>name<tool_sep>...<arg_key>...<arg_value>...</tool_call>...
tool_call_blocks = re.findall(rf'<tool_call{self.HYTK}>(.*?)</tool_call{self.HYTK}>', response, re.DOTALL)
functions = []
for block in tool_call_blocks:
# Extract function name: text before <tool_sep>
name_match = re.match(rf'(.*?)<tool_sep{self.HYTK}>', block, re.DOTALL)
if not name_match:
continue
name = name_match.group(1).strip()
# Extract arg_key/arg_value pairs together to avoid misalignment
pairs = re.findall(
rf'<arg_key{self.HYTK}>(.*?)</arg_key{self.HYTK}>\s*<arg_value{self.HYTK}>(.*?)</arg_value{self.HYTK}>',
block, re.DOTALL)
arguments = {}
for k, v in pairs:
k = k.strip()
v = v.strip()
parsed = self._parse_json(v)
arguments[k] = parsed if parsed is not None else v
functions.append(Function(name=name, arguments=arguments))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response{self.HYTK}>\n{tool_content}\n</tool_response{self.HYTK}>')
tool_responses = '\n'.join(res_tool)
return f'<tool_responses{self.HYTK}>\n{tool_responses}\n</tool_responses{self.HYTK}>'
def _get_tool_calls(self, tool_calls: List[str]):
tool_calls_str = '\n'.join(tool_calls)
return f'<tool_calls{self.HYTK}>\n{tool_calls_str}\n</tool_calls{self.HYTK}>'
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
res = [f'<hy_eos{self.HYTK}>', self._get_tool_responses(tool_messages), f'<hy_Assistant{self.HYTK}>']
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
system = system or ''
if system:
system = f'{system}\n\n'
return f"""{system}# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
""" + '\n'.join(tool_descs) + f"""
</tools>
For function call returns, you should first print <tool_calls{self.HYTK}>
For each function call, you should return object like:
<tool_call{self.HYTK}>{{function-name}}<tool_sep{self.HYTK}>
<arg_key{self.HYTK}>{{arg-key-1}}</arg_key{self.HYTK}>
<arg_value{self.HYTK}>{{arg-value-1}}</arg_value{self.HYTK}>
<arg_key{self.HYTK}>{{arg-key-2}}</arg_key{self.HYTK}>
<arg_value{self.HYTK}>{{arg-value-2}}</arg_value{self.HYTK}>
...
</tool_call{self.HYTK}>
At the end of function call returns, you should print </tool_calls{self.HYTK}>"""
def _format_tool_calls(self, tool_call_messages):
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = tool_call['arguments']
arg_lines = []
if isinstance(arguments, dict):
for k, v in arguments.items():
if not isinstance(v, str):
v = json.dumps(v, ensure_ascii=False)
arg_lines.append(f'<arg_key{self.HYTK}>{k}</arg_key{self.HYTK}>\n'
f'<arg_value{self.HYTK}>{v}</arg_value{self.HYTK}>')
arg_str = '\n'.join(arg_lines)
tool_calls.append(f'<tool_call{self.HYTK}>{name}<tool_sep{self.HYTK}>\n{arg_str}\n</tool_call{self.HYTK}>')
return self._get_tool_calls(tool_calls)
class HyV3AgentTemplate(HyV3PreviewAgentTemplate):
HYTK = ':opensource'
+119
View File
@@ -0,0 +1,119 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class KimiK25AgentTemplate(BaseAgentTemplate):
"""Agent template for Kimi K2.5/K2.6 models.
Tool calling format:
- Tools are declared in a separate system message with role 'tool_declare'
using TypeScript namespace format.
- Tool calls:
<|tool_calls_section_begin|>
<|tool_call_begin|>{function_name}<|tool_call_argument_begin|>{args_json}<|tool_call_end|>
<|tool_calls_section_end|>
- Tool response:
<|im_system|>tool<|im_middle|>## Return of {tool_call_id}
{content}<|im_end|>
"""
@staticmethod
def _json_type_to_ts(json_type):
type_map = {
'string': 'string',
'number': 'number',
'integer': 'number',
'boolean': 'boolean',
'array': 'any[]',
'object': 'object',
'null': 'null',
}
return type_map.get(json_type, 'any')
def _tools_to_typescript(self, tools):
parts = []
for tool in tools:
tool = self.unwrap_tool(tool)
name = self._get_tool_name(tool)
description = tool.get('description', '')
parameters = tool.get('parameters', {})
properties = parameters.get('properties', {})
lines = []
if description:
lines.append(f'// {description}')
if not properties:
lines.append(f'type {name} = (_: {{}}) => any;')
else:
lines.append(f'type {name} = (_: {{')
props = list(properties.items())
for i, (pname, pschema) in enumerate(props):
pdesc = pschema.get('description', '')
ptype = self._json_type_to_ts(pschema.get('type', ''))
if pdesc:
lines.append(f' // {pdesc}')
if i < len(props) - 1:
lines.append(f' {pname}: {ptype},')
else:
lines.append(f' {pname}: {ptype}')
lines.append('}) => any;')
parts.append('\n'.join(lines))
return '\n'.join(parts)
def get_toolcall(self, response: str) -> List[Function]:
pattern = r'<\|tool_call_begin\|>(.*?)<\|tool_call_argument_begin\|>(.*?)<\|tool_call_end\|>'
res_list = re.findall(pattern, response, re.DOTALL)
functions = []
for name, arguments in res_list:
name = name.strip()
arguments = arguments.strip()
parsed_args = self._parse_json(arguments)
if parsed_args is not None:
functions.append(Function(name=name, arguments=parsed_args))
else:
functions.append(Function(name=name, arguments=arguments))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
ts_tools = self._tools_to_typescript(tools)
tool_content = f'# Tools\n\n## functions\nnamespace functions {{\n{ts_tools}\n}}\n'
system = system or ''
res = f'tool_declare<|im_middle|>{tool_content}'
if system:
res += f'<|im_end|><|im_system|>system<|im_middle|>{system}'
return res
def _format_tool_calls(self, tool_call_messages) -> str:
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = json.dumps(tool_call['arguments'], ensure_ascii=False)
tool_calls.append(f'<|tool_call_begin|>{name}<|tool_call_argument_begin|>{arguments}<|tool_call_end|>')
return f'<|tool_calls_section_begin|>{"".join(tool_calls)}<|tool_calls_section_end|>'
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
res = ['<|im_end|>']
for tool_message in tool_messages:
tool_call_id = tool_message.get('tool_call_id', '')
tool_content = tool_message['content']
res.append(f'<|im_system|>tool<|im_middle|>## Return of {tool_call_id}\n{tool_content}<|im_end|>')
res.append('<|im_assistant|>assistant<|im_middle|>')
return assistant_content, res
+74
View File
@@ -0,0 +1,74 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class Llama3AgentTemplate(BaseAgentTemplate):
eom_token = '<|eom_id|>'
start_token = '<|start_header_id|>'
end_token = '<|end_header_id|>'
eot_token = '<|eot_id|>'
def get_toolcall(self, response: str) -> List[Function]:
if response.endswith(self.eom_token):
response = response[:-len(self.eom_token)]
functions = []
res_list = re.findall(r'{[^{]*?"name":.*?"parameters":\s*?{.*?}\s*?}', response, re.DOTALL)
for res in res_list:
res = self._parse_json(res)
if isinstance(res, dict) and 'name' in res and 'parameters' in res:
functions.append(Function(name=res['name'], arguments=res['parameters']))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
res = [self.eot_token]
for tool_message in tool_messages:
tool_content = tool_message['content']
res.append(f'{self.start_token}tool{self.end_token}\n\n{tool_content}{self.eot_token}')
res.append(f'{self.start_token}assistant{self.end_token}\n\n')
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
assert user_message is not None
user_content = user_message['content']
tool_descs = [json.dumps(tool, ensure_ascii=False, indent=4) for tool in tools]
new_user_content = """Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.
Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}. Do not use variables.
""" + '\n\n'.join(tool_descs) + f"""
{user_content}""" # noqa
user_message['content'] = new_user_content
return system or ''
def _format_tool_calls(self, tool_call_messages) -> str:
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
tool_call['parameters'] = tool_call.pop('arguments')
tool_calls.append(json.dumps(tool_call, ensure_ascii=False))
return '\n'.join(tool_calls)
class Llama4AgentTemplate(Llama3AgentTemplate):
eom_token = '<|eom|>'
start_token = '<|header_start|>'
end_token = '<|header_end|>'
eot_token = '<|eot|>'
toolcall_pattern = r'(.+?)<\|eom\|>'
+61
View File
@@ -0,0 +1,61 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
from .deepseek_v3_1 import DeepSeekV31AgentTemplate
from .deepseek_v4 import DeepSeekV4AgentTemplate
from .extra import ReactGRPOAgentTemplate
from .gemma4 import Gemma4AgentTemplate
from .glm4 import (ChatGLM4AgentTemplate, GLM4_5AgentTemplate, GLM4_7AgentTemplate, GLM4AgentTemplate,
GLM5_1AgentTemplate)
from .hermes import HermesAgentTemplate, HunyuanHermesAgentTemplate
from .hy_v3 import HyV3AgentTemplate, HyV3PreviewAgentTemplate
from .kimi_k25 import KimiK25AgentTemplate
from .llama import Llama3AgentTemplate, Llama4AgentTemplate
from .minicpm5 import MiniCPM5AgentTemplate
from .minimax_m2 import MinimaxM2AgentTemplate
from .minimax_m3 import MinimaxM3AgentTemplate
from .mistral import MistralAgentTemplate
from .qwen import QwenEnAgentTemplate, QwenEnParallelAgentTemplate, QwenZhAgentTemplate, QwenZhParallelAgentTemplate
from .qwen3_coder import Qwen3_5AgentTemplate, Qwen3CoderAgentTemplate
from .react import ReactEnAgentTemplate, ReactZnAgentTemplate
from .seed_oss import SeedAgentTemplate
from .toolbench import ToolBenchAgentTemplate
from .youtu import YoutuAgentTemplate
agent_template_map = {
# ref: https://qwen.readthedocs.io/zh-cn/latest/framework/function_call.html#function-calling-templates
'react_en': ReactEnAgentTemplate,
'react_zh': ReactZnAgentTemplate,
# ref: https://github.com/QwenLM/Qwen-Agent/blob/main/qwen_agent/llm/fncall_prompts/qwen_fncall_prompt.py
'qwen_en': QwenEnAgentTemplate,
'qwen_zh': QwenZhAgentTemplate,
'qwen_en_parallel': QwenEnParallelAgentTemplate,
'qwen_zh_parallel': QwenZhParallelAgentTemplate,
'qwen3_coder': Qwen3CoderAgentTemplate,
'qwen3_5': Qwen3_5AgentTemplate,
'hermes': HermesAgentTemplate,
'hunyuan_hermes': HunyuanHermesAgentTemplate,
'hy_v3_preview': HyV3PreviewAgentTemplate,
'hy_v3': HyV3AgentTemplate,
'toolbench': ToolBenchAgentTemplate, # ref: https://modelscope.cn/datasets/swift/ToolBench
'chatglm4': ChatGLM4AgentTemplate,
'glm4': GLM4AgentTemplate, # ref: https://modelscope.cn/models/ZhipuAI/GLM-4-9B-0414
'glm4_5': GLM4_5AgentTemplate,
'glm4_7': GLM4_7AgentTemplate,
'glm5_1': GLM5_1AgentTemplate,
'llama3': Llama3AgentTemplate,
'llama4': Llama4AgentTemplate,
# ref: https://huggingface.co/deepseek-ai/DeepSeek-V3.1
'deepseek_v3_1': DeepSeekV31AgentTemplate,
# ref: https://modelscope.cn/models/deepseek-ai/DeepSeek-V4-Flash
'deepseek_v4': DeepSeekV4AgentTemplate,
'minimax_m2': MinimaxM2AgentTemplate,
'minimax_m3': MinimaxM3AgentTemplate,
'seed_oss': SeedAgentTemplate,
# ref: https://modelscope.cn/models/google/gemma-4-12B-it
'gemma4': Gemma4AgentTemplate,
# extra
'react_grpo': ReactGRPOAgentTemplate,
'mistral': MistralAgentTemplate,
'youtu': YoutuAgentTemplate,
'kimi_k25': KimiK25AgentTemplate,
'minicpm5': MiniCPM5AgentTemplate,
}
+114
View File
@@ -0,0 +1,114 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class MiniCPM5AgentTemplate(BaseAgentTemplate):
"""Agent template for MiniCPM5 models using XML-based function calling format.
Tool call format:
<function name="function-name"><param name="param-name">param-value</param></function>
Tool response format:
<tool_response>
response_content
</tool_response>
"""
def get_toolcall(self, response: str) -> List[Function]:
# Match <function name="...">...</function> blocks
func_pattern = re.compile(r'<function\s+name="([^"]+)">(.*?)</function>', re.DOTALL)
param_pattern = re.compile(r'<param\s+name="([^"]+)">'
r'(?:<!\[CDATA\[(.*?)\]\]>|([^<]*))'
r'</param>', re.DOTALL)
functions = []
for func_match in func_pattern.finditer(response):
func_name = func_match.group(1)
func_body = func_match.group(2)
arguments = {}
for param_match in param_pattern.finditer(func_body):
param_name = param_match.group(1)
# CDATA value or plain value
param_value = param_match.group(2) if param_match.group(2) is not None else param_match.group(3)
# Try to parse as JSON value (number, bool, etc.)
try:
param_value = json.loads(param_value)
except (json.JSONDecodeError, ValueError):
pass
arguments[param_name] = param_value
functions.append(Function(name=func_name, arguments=arguments))
if len(functions) == 0:
# Fallback to ReAct-style parsing
return super().get_toolcall(response)
return functions
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response>\n{tool_content}\n</tool_response>')
return '\n'.join(res_tool)
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
if hasattr(self, 'template_meta'):
prompt = self.template_meta.prompt
chat_sep = self.template_meta.chat_sep
else:
prompt = ['<|im_start|>user\n{{QUERY}}<|im_end|>\n<|im_start|>assistant\n']
chat_sep = ['<|im_end|>\n']
res = chat_sep.copy()
total_tool = self._get_tool_responses(tool_messages)
for context in prompt:
if isinstance(context, str):
context = context.replace('{{QUERY}}', total_tool)
res.append(context)
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
system = system or ''
if system:
system = f'{system}\n\n'
return (f'{system}# Tools\n\n'
'You are provided with function signatures within <tools></tools> XML tags:\n'
'<tools>\n' + '\n'.join(tool_descs) + '\n</tools>\n\n'
'Tool 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>')
def _format_tool_calls(self, tool_call_messages) -> str:
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = tool_call['arguments']
params_xml = ''
if isinstance(arguments, dict):
for param_name, param_value in arguments.items():
value_str = param_value if isinstance(param_value, str) else json.dumps(
param_value, ensure_ascii=False)
if isinstance(param_value, str) and ('<' in param_value or '&' in param_value
or '\n' in param_value):
params_xml += f'<param name="{param_name}"><![CDATA[{value_str}]]></param>'
else:
params_xml += f'<param name="{param_name}">{value_str}</param>'
tool_calls.append(f'<function name="{name}">{params_xml}</function>')
return '\n'.join(tool_calls)
+171
View File
@@ -0,0 +1,171 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class MinimaxM2AgentTemplate(BaseAgentTemplate):
"""
Agent template for MiniMax-M2 series models.
This template handles tool calling in MiniMax's XML-based format:
<minimax:tool_call>
<invoke name="tool-name">
<parameter name="param-key">param-value</parameter>
</invoke>
</minimax:tool_call>
"""
def get_toolcall(self, response: str) -> List[Function]:
"""
Extract tool calls from MiniMax response format.
Format:
<minimax:tool_call>
<invoke name="tool-name">
<parameter name="param-key">param-value</parameter>
</invoke>
</minimax:tool_call>
"""
functions = []
# Find all tool_call blocks
tool_call_blocks = re.findall(r'<minimax:tool_call>(.*?)</minimax:tool_call>', response, re.DOTALL)
for block in tool_call_blocks:
# Find all invoke blocks within the tool_call
invoke_blocks = re.findall(r'<invoke name="([^"]+)">(.*?)</invoke>', block, re.DOTALL)
for tool_name, params_block in invoke_blocks:
# Extract parameters
params = {}
param_matches = re.findall(r'<parameter name="([^"]+)">(.*?)</parameter>', params_block, re.DOTALL)
for param_name, param_value in param_matches:
param_value = param_value.strip()
# Try to parse as JSON if it looks like a JSON structure
parsed_value = self._parse_json(param_value)
params[param_name] = parsed_value if parsed_value is not None else param_value
functions.append(Function(name=tool_name, arguments=params))
# Fallback to react format if no functions found
if len(functions) == 0:
return super().get_toolcall(response)
return functions
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
"""
Format tool execution results in MiniMax format.
Tool responses are wrapped in <response></response> tags.
"""
# Check if using react format
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
# Use template meta if available
if hasattr(self, 'template_meta'):
prompt = self.template_meta.prompt.copy()
chat_sep = self.template_meta.chat_sep
for i in range(len(prompt)):
if isinstance(prompt[i], str):
prompt[i] = prompt[i].replace('user', 'tool')
else:
# Default format based on the Jinja2 template
prompt = [']~b]tool\n{{QUERY}}[e~[\n']
chat_sep = ['[e~[\n']
res = chat_sep.copy()
# Format tool responses
tool_responses = []
for tool_message in tool_messages:
tool_content = tool_message['content']
tool_responses.append(f'<response>{tool_content}</response>')
total_tool = '\n'.join(tool_responses)
for context in prompt:
if isinstance(context, str):
context = context.replace('{{QUERY}}', total_tool)
res.append(context)
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
"""
Format tools in MiniMax format with JSONSchema and XML invocation examples.
"""
# Parse tools to JSONSchema format
tool_schemas = []
for tool in tools:
tool = self.unwrap_tool(tool)
tool_schemas.append(json.dumps(tool, ensure_ascii=False))
system = system or ''
return f"""{system}
# Tools
You may call one or more tools to assist with the user query.
Here are the tools available in JSONSchema format:
<tools>
""" + '\n'.join(f'<tool>{schema}</tool>' for schema in tool_schemas) + """
</tools>
When making tool calls, use XML format to invoke tools and pass parameters:
<minimax:tool_call>
<invoke name="tool-name-1">
<parameter name="param-key-1">param-value-1</parameter>
<parameter name="param-key-2">param-value-2</parameter>
...
</invoke>
</minimax:tool_call>"""
def _format_tool_calls(self, tool_call_messages):
"""
Format tool call messages into MiniMax XML format.
Args:
tool_call_messages: List of messages containing tool call information.
Returns:
Formatted string with tool calls in MiniMax XML format.
"""
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = tool_call['arguments']
# Build parameter list
params = []
for key, value in arguments.items():
# Convert value to JSON string if it's not a string
if not isinstance(value, str):
value = json.dumps(value, ensure_ascii=False)
params.append(f'<parameter name="{key}">{value}</parameter>')
# Build invoke block
invoke_block = f'<invoke name="{name}">\n' + '\n'.join(params) + '\n</invoke>'
tool_calls.append(invoke_block)
# Wrap all invocations in tool_call tags
if tool_calls:
return '<minimax:tool_call>\n' + '\n'.join(tool_calls) + '\n</minimax:tool_call>'
return ''
+235
View File
@@ -0,0 +1,235 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import Any, List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
# Special token used as a namespace prefix for every XML tag in MiniMax-M3
# tool_call payloads.
NS_TOKEN = ']<]minimax[>['
TOOLCALL_BEGIN_TOKEN = NS_TOKEN + '<tool_call>'
TOOLCALL_END_TOKEN = NS_TOKEN + '</tool_call>'
def _to_xml(val: Any, ns: str = NS_TOKEN) -> str:
"""Recursive XML renderer mirroring the ``to_xml`` macro in MiniMax-M3's
``chat_template.jinja``.
``None`` values are intentionally omitted (consistent with the upstream
convention that drops ``None`` parameters rather than emitting a literal
``null`` string).
"""
if val is None:
return ''
if isinstance(val, dict):
parts = []
for k, v in val.items():
if v is None:
continue
parts.append(f'{ns}<{k}>{_to_xml(v, ns)}{ns}</{k}>')
return ''.join(parts)
if isinstance(val, (list, tuple)):
parts = []
for item in val:
parts.append(f'{ns}<item>{_to_xml(item, ns)}{ns}</item>')
return ''.join(parts)
if isinstance(val, bool):
return json.dumps(val)
return str(val)
_NS = re.escape(NS_TOKEN)
_TC_BEGIN = re.escape(TOOLCALL_BEGIN_TOKEN)
_TC_END = re.escape(TOOLCALL_END_TOKEN)
# Match any opening tag like ]<]minimax[>[<key> (excluding closing/invoke/tool_call)
_INVOKE_RE = re.compile(rf'{_NS}<invoke\s+name="([^"]+)">(.*?){_NS}</invoke>', re.DOTALL)
_TOOLCALL_RE = re.compile(rf'{_TC_BEGIN}(.*?){_TC_END}', re.DOTALL)
def _parse_xml_value(content: str) -> Any:
"""Parse an XML fragment produced by ``to_xml`` back into a Python value.
The expected fragments use ``NS_TOKEN`` as a tag prefix. The function
handles nested ``<item>`` lists, dict-like ``<key>...</key>`` structures
and falls back to a stripped string for primitive payloads.
"""
content = content.strip()
if not content:
return ''
# Try list of items first (heuristic: starts with `<item>`).
if content.startswith(f'{NS_TOKEN}<item>'):
items = []
for inner in _iter_tagged(content, 'item'):
items.append(_parse_xml_value(inner))
return items
# Try mapping (heuristic: starts with a NS_TOKEN<tag>).
if content.startswith(NS_TOKEN + '<'):
result: dict = {}
for key, inner in _iter_keyed(content):
result[key] = _parse_xml_value(inner)
if result:
return result
# Primitive fallback. Try JSON (booleans / numbers) before raw text.
try:
return json.loads(content)
except Exception:
return content
def _iter_tagged(content: str, tag: str):
pattern = re.compile(rf'{_NS}<{re.escape(tag)}>(.*?){_NS}</{re.escape(tag)}>', re.DOTALL)
for m in pattern.finditer(content):
yield m.group(1)
def _iter_keyed(content: str):
"""Iterate ``(tag_name, inner_content)`` for top-level NS-prefixed tags."""
cursor = 0
n = len(content)
open_pat = re.compile(rf'{_NS}<([^/!?\s>]+)>')
while cursor < n:
m = open_pat.search(content, cursor)
if not m:
return
name = m.group(1)
end_marker = f'{NS_TOKEN}</{name}>'
# Match nested same-name tags by counting depth.
depth = 1
scan = m.end()
open_marker = f'{NS_TOKEN}<{name}>'
while depth > 0 and scan < n:
next_open = content.find(open_marker, scan)
next_close = content.find(end_marker, scan)
if next_close == -1:
return
if next_open != -1 and next_open < next_close:
depth += 1
scan = next_open + len(open_marker)
else:
depth -= 1
scan = next_close + len(end_marker)
inner = content[m.end():scan - len(end_marker)]
yield name, inner
cursor = scan
class MinimaxM3AgentTemplate(BaseAgentTemplate):
"""Agent template for MiniMax-M3 series multimodal models.
Tool calls follow this XML-with-namespace format:
]<]minimax[>[<tool_call>
]<]minimax[>[<invoke name="tool-name">
]<]minimax[>[<param-1>value-1]<]minimax[>[</param-1>
]<]minimax[>[<param-2>]<]minimax[>[<item>...]<]minimax[>[</item>]<]minimax[>[</param-2>
]<]minimax[>[</invoke>
]<]minimax[>[</tool_call>
Tool responses are wrapped in ``<response>...</response>`` inside a
``]~b]tool`` slot.
"""
def get_toolcall(self, response: str) -> List[Function]:
functions: List[Function] = []
for tc_block in _TOOLCALL_RE.findall(response):
for tool_name, params_block in _INVOKE_RE.findall(tc_block):
arguments = {}
for key, inner in _iter_keyed(params_block):
arguments[key] = _parse_xml_value(inner)
functions.append(Function(name=tool_name, arguments=arguments))
if not functions:
return super().get_toolcall(response)
return functions
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
if hasattr(self, 'template_meta'):
prompt = self.template_meta.prompt.copy()
chat_sep = self.template_meta.chat_sep
for i in range(len(prompt)):
if isinstance(prompt[i], str):
prompt[i] = prompt[i].replace('user', 'tool')
else:
prompt = [']~b]tool\n{{QUERY}}[e~[\n]~b]ai\n']
chat_sep = ['[e~[\n']
res = chat_sep.copy() if chat_sep else []
tool_responses = []
for tool_message in tool_messages:
tool_content = tool_message['content']
tool_responses.append(f'<response>{tool_content}</response>')
total_tool = '\n'.join(tool_responses)
for context in prompt:
if isinstance(context, str):
context = context.replace('{{QUERY}}', total_tool)
res.append(context)
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_schemas = []
for tool in tools:
tool = self.unwrap_tool(tool)
tool_schemas.append(json.dumps(tool, ensure_ascii=False))
system = system or ''
tools_xml = '\n'.join(f'<tool>{schema}</tool>' for schema in tool_schemas)
# Mirror the example block produced by chat_template.jinja so the
# in-context format hint matches inference time exactly.
# Note: jinja emits 'Example:\n' then '\n' before the tool_call_begin
# token, which renders as two consecutive newlines.
example = (f'\n\n{TOOLCALL_BEGIN_TOKEN}\n'
f'{NS_TOKEN}<invoke name="tool-name-1">'
f'{NS_TOKEN}<param-1>value-1{NS_TOKEN}</param-1>'
f'{NS_TOKEN}<param-2>'
f'{NS_TOKEN}<item>'
f'{NS_TOKEN}<key-a>val-a{NS_TOKEN}</key-a>'
f'{NS_TOKEN}<key-b>val-b{NS_TOKEN}</key-b>'
f'{NS_TOKEN}</item>'
f'{NS_TOKEN}</param-2>'
f'{NS_TOKEN}</invoke>\n'
f'{NS_TOKEN}<invoke name="tool-name-2">'
f'{NS_TOKEN}<param-1>value-1{NS_TOKEN}</param-1>'
f'{NS_TOKEN}</invoke>\n'
f'{TOOLCALL_END_TOKEN}')
return (f'{system}\n\n# Tools\n'
'You may call one or more tools to assist with the user query.\n'
'Here are the tools available in JSONSchema format:\n'
f'\n<tools>\n{tools_xml}\n</tools>\n\n'
f'To call tools, wrap all invocations in a single {TOOLCALL_BEGIN_TOKEN}{TOOLCALL_END_TOKEN} '
'block. Parameter values containing nested objects or arrays are recursively expanded into '
f'XML elements. Example:{example}')
def _format_tool_calls(self, tool_call_messages) -> str:
invocations = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
name = tool_call['name']
arguments = tool_call['arguments'] or {}
param_parts = [f'{NS_TOKEN}<invoke name="{name}">']
for k, v in arguments.items():
if v is None:
continue
param_parts.append(f'{NS_TOKEN}<{k}>{_to_xml(v, NS_TOKEN)}{NS_TOKEN}</{k}>')
param_parts.append(f'{NS_TOKEN}</invoke>')
invocations.append(''.join(param_parts))
if not invocations:
return ''
return f'{TOOLCALL_BEGIN_TOKEN}\n' + '\n'.join(invocations) + f'\n{TOOLCALL_END_TOKEN}'
+67
View File
@@ -0,0 +1,67 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class MistralAgentTemplate(BaseAgentTemplate):
def get_toolcall(self, response: str) -> List[Function]:
res_list = re.findall(r'\[TOOL_CALLS\]\[(.*?)\]</s>', response, re.DOTALL)
if not res_list:
return []
res_list = res_list[0].strip().split('\n')
functions = []
for res_str in res_list:
parsed_res = self._parse_json(res_str)
if isinstance(parsed_res, dict):
parsed_res = [parsed_res] # Handle single tool call
if isinstance(parsed_res, list):
for tool_call in parsed_res:
if isinstance(tool_call, dict) and 'name' in tool_call and 'arguments' in tool_call:
functions.append(Function(name=tool_call['name'], arguments=tool_call['arguments']))
if len(functions) == 0:
# compat react_en
return super().get_toolcall(response)
return functions
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
if not hasattr(self, 'template_meta'):
raise ValueError('MistralAgentTemplate requires template_meta to be registered')
prompt = self.template_meta.prompt
chat_sep = self.template_meta.chat_sep
res = chat_sep.copy()
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
# append `[TOOL_RESULTS]{"content": {{ .Content }}}[/TOOL_RESULTS]` to res_tool
res_tool.append(f'[TOOL_RESULTS]{json.dumps({"content": tool_content}, ensure_ascii=False)}[/TOOL_RESULTS]')
total_tool = '\n'.join(res_tool)
for context in prompt:
if isinstance(context, str):
context = context.replace('{{QUERY}}', total_tool)
res.append(context)
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
system = system or ''
return f"""{system}[AVAILABLE_TOOLS]{' '.join(tool_descs)}[/AVAILABLE_TOOLS]"""
def _format_tool_calls(self, tool_call_messages):
tool_calls = []
for message in tool_call_messages:
# needs `{'name': name, 'arguments': arguments}`, which self._parse_tool_call
# satisfies
tool_call = self._parse_tool_call(message['content'])
tool_calls.append(json.dumps(tool_call, ensure_ascii=False))
return f'[TOOL_CALLS][\n{chr(10).join(tool_calls)}\n]</s>' # check if need `</s>` at end
+134
View File
@@ -0,0 +1,134 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
from typing import List, Optional, Union
from .base import AgentKeyword, BaseAgentTemplate
keyword = AgentKeyword(
action='✿FUNCTION✿:',
action_input='✿ARGS✿:',
observation='✿RESULT✿:',
)
class QwenEnAgentTemplate(BaseAgentTemplate):
keyword = keyword
def _get_tool_names_descs(self, tools):
tool_names = []
tool_descs = []
for tool in tools:
tool_desc = self._parse_tool(tool, 'en')
tool_names.append(tool_desc.name_for_model)
tool_descs.append(f'### {tool_desc.name_for_human}\n\n'
f'{tool_desc.name_for_model}: {tool_desc.description_for_model} '
f'Parameters: {tool_desc.parameters} {tool_desc.args_format}')
return tool_names, tool_descs
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names, tool_descs = self._get_tool_names_descs(tools)
system = system or ''
return f"""{system}
# Tools
## You have access to the following tools:
""" + '\n\n'.join(tool_descs) + f"""
## When you need to call a tool, please insert the following command in your reply, which can be called zero or multiple times according to your needs:
✿FUNCTION✿: The tool to use, should be one of [{','.join(tool_names)}]
✿ARGS✿: The input of the tool
✿RESULT✿: Tool results
✿RETURN✿: Reply based on tool results. Images need to be rendered as ![](url)""" # noqa
class QwenZhAgentTemplate(BaseAgentTemplate):
keyword = keyword
def _get_tool_names_descs(self, tools):
tool_names = []
tool_descs = []
for tool in tools:
tool_desc = self._parse_tool(tool, 'zh')
tool_names.append(tool_desc.name_for_model)
tool_descs.append(f'### {tool_desc.name_for_human}\n\n'
f'{tool_desc.name_for_model}: {tool_desc.description_for_model} '
f'输入参数:{tool_desc.parameters} {tool_desc.args_format}')
return tool_names, tool_descs
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names, tool_descs = self._get_tool_names_descs(tools)
system = system or ''
return f"""{system}
# 工具
## 你拥有如下工具:
""" + '\n\n'.join(tool_descs) + f"""
## 你可以在回复中插入零次、一次或多次以下命令以调用工具:
✿FUNCTION✿: 工具名称,必须是[{','.join(tool_names)}]之一。
✿ARGS✿: 工具输入
✿RESULT✿: 工具结果
✿RETURN✿: 根据工具结果进行回复,需将图片用![](url)渲染出来""" # noqa
class QwenEnParallelAgentTemplate(QwenEnAgentTemplate):
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names, tool_descs = self._get_tool_names_descs(tools)
system = system or ''
return f"""{system}
# Tools
## You have access to the following tools:
""" + '\n\n'.join(tool_descs) + f"""
## Insert the following command in your reply when you need to call N tools in parallel:
✿FUNCTION✿: The name of tool 1, should be one of [{','.join(tool_names)}]
✿ARGS✿: The input of tool 1
✿FUNCTION✿: The name of tool 2
✿ARGS✿: The input of tool 2
...
✿FUNCTION✿: The name of tool N
✿ARGS✿: The input of tool N
✿RESULT✿: The result of tool 1
✿RESULT✿: The result of tool 2
...
✿RESULT✿: he result of tool N
✿RETURN✿: Reply based on tool results. Images need to be rendered as ![](url)""" # noqa
class QwenZhParallelAgentTemplate(QwenZhAgentTemplate):
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names, tool_descs = self._get_tool_names_descs(tools)
system = system or ''
return f"""{system}
# 工具
## 你拥有如下工具:
""" + '\n\n'.join(tool_descs) + f"""
## 你可以在回复中插入以下命令以并行调用N个工具:
✿FUNCTION✿: 工具1的名称,必须是[{','.join(tool_names)}]之一
✿ARGS✿: 工具1的输入
✿FUNCTION✿: 工具2的名称
✿ARGS✿: 工具2的输入
...
✿FUNCTION✿: 工具N的名称
✿ARGS✿: 工具N的输入
✿RESULT✿: 工具1的结果
✿RESULT✿: 工具2的结果
...
✿RESULT✿: 工具N的结果
✿RETURN✿: 根据工具结果进行回复,需将图片用![](url)渲染出来""" # noqa
+200
View File
@@ -0,0 +1,200 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Union
from swift.infer_engine import Function
from .hermes import HermesAgentTemplate
def render_extra_keys(obj, handled_keys):
"""Helper function to render extra keys not explicitly handled"""
result = ''
if isinstance(obj, dict):
for key, value in obj.items():
if key not in handled_keys:
result += f'\n<{key}>{json.dumps(value, ensure_ascii=False)}</{key}>'
return result
TOOL_DESC_SUFFIX = (
'</tools>\n\nIf you choose to call a function ONLY reply in the following format with '
'NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\n'
'value_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\n'
'that can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\n'
'Reminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> '
'block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n'
'- You may provide optional reasoning for your function call in natural language BEFORE the function call, '
'but NOT after\n- If there is no function call available, '
'answer the question like normal with your current '
'knowledge and do not tell the user about function calls\n</IMPORTANT>')
class Qwen3CoderAgentTemplate(HermesAgentTemplate):
@staticmethod
def _find_function_call(single_content: str) -> Optional[Function]:
single_content = single_content.strip()
# Check whether the complete function tag is included
if not single_content.startswith('<function=') or not single_content.endswith('</function>'):
return None
# Extract function name
func_name_match = re.search(r'<function=([^>]+)>', single_content)
if not func_name_match:
return None
func_name = func_name_match.group(1).strip()
parameters = {}
# Use regular expressions to match parameters
# Match any content of <parameter=name>content</parameter>
param_pattern = r'<parameter=([^>]+)>\s*(.*?)\s*</parameter>'
param_matches = re.findall(param_pattern, single_content, re.DOTALL)
for param_name, param_value in param_matches:
# Clear the parameter values and remove any possible additional whitespace
clean_value = param_value.strip()
parameters[param_name.strip()] = clean_value
return Function(name=func_name, arguments=json.dumps(parameters, ensure_ascii=False))
def get_toolcall(self, response: str) -> List[Function]:
# Extract the tool call parameters from the model's response
toolcall_list = re.findall(r'<tool_call>(.*?)</tool_call>', response, re.DOTALL)
functions = []
for toolcall in toolcall_list:
function = self._find_function_call(toolcall)
if function:
functions.append(function)
if len(functions) == 0:
# Compat react_en
return super(HermesAgentTemplate, self).get_toolcall(response)
return functions
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
if system is None:
system = 'You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks.'
tool_descs = [f'{system}\n\n# Tools\n\nYou have access to the following functions:\n\n<tools>']
for tool in tools:
tool_desc = ''
# Check function key
if isinstance(tool, dict) and 'function' in tool:
tool = tool['function']
# Add function name
tool_desc += f"<function>\n<name>{tool['name']}</name>"
# Add description if available
if 'description' in tool:
tool_desc += f"\n<description>{tool['description'].strip()}</description>"
# Add parameters section
tool_desc += '\n<parameters>'
# Process parameters if they exist in the expected structure
if ('parameters' in tool and isinstance(tool['parameters'], dict) and 'properties' in tool['parameters']
and isinstance(tool['parameters']['properties'], dict)):
for param_name, param_fields in tool['parameters']['properties'].items():
tool_desc += '\n<parameter>'
tool_desc += f'\n<name>{param_name}</name>'
if 'type' in param_fields:
tool_desc += f"\n<type>{str(param_fields['type'])}</type>"
if 'description' in param_fields:
tool_desc += f"\n<description>{param_fields['description'].strip()}</description>"
# Add any extra parameter fields
handled_param_keys = ['name', 'type', 'description']
tool_desc += render_extra_keys(param_fields, handled_param_keys)
tool_desc += '\n</parameter>'
# Add any extra parameter section fields
handled_keys = ['type', 'properties']
if 'parameters' in tool:
tool_desc += render_extra_keys(tool['parameters'], handled_keys)
tool_desc += '\n</parameters>'
# Add any extra function fields
handled_keys = ['type', 'name', 'description', 'parameters']
tool_desc += render_extra_keys(tool, handled_keys)
tool_desc += '\n</function>'
tool_descs.append(tool_desc)
tool_descs.append(TOOL_DESC_SUFFIX)
tool_descs = '\n'.join(tool_descs)
return tool_descs
def _format_tool_calls(self, tool_call_messages):
result_parts = []
for idx, message in enumerate(tool_call_messages):
tool_call = self._parse_tool_call(message['content'])
result_parts.append(f"<tool_call>\n<function={tool_call['name']}>\n")
# Processing parameters (if present)
if 'arguments' in tool_call and tool_call['arguments']:
for args_name, args_value in tool_call['arguments'].items():
result_parts.append(f'<parameter={args_name}>\n')
# Handle different types of parameter values
if isinstance(args_value, (dict, list)):
# For dictionaries or lists, use json formatting
args_value = json.dumps(args_value, ensure_ascii=False)
else:
# For other types, convert to strings
args_value = str(args_value)
result_parts.append(f'{args_value}\n</parameter>\n')
# Close tags
result_parts.append('</function>\n</tool_call>')
# ref: https://github.com/QwenLM/Qwen3-Coder/blob/0ae30f55e9d6c47ff763c334f99c135ad68915dd/qwencoder-eval/tool_calling_eval/berkeley-function-call-leaderboard/bfcl_eval/model_handler/local_inference/qwen_fc.py#L21 # noqa
if idx != len(tool_call_messages) - 1:
result_parts.append('\n')
return ''.join(result_parts)
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response>\n{tool_content}\n</tool_response>\n')
return ''.join(res_tool)
class Qwen3_5AgentTemplate(Qwen3CoderAgentTemplate):
def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
"""Qwen3.5/3.6 jinja inserts \n\n between assistant content and <tool_call>
only when effective content (after think removal) is non-empty."""
if not pre_message or pre_message.get('role') != 'assistant':
return tool_content
content = pre_message.get('content', '')
if not isinstance(content, str):
return tool_content
# Mirror jinja: content.split('</think>')[-1].lstrip('\n') then content|trim
if '</think>' in content:
effective = content.split('</think>')[-1].lstrip('\n')
else:
effective = content
if effective.strip():
return '\n\n' + tool_content
return tool_content
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
tools_prompt = """# Tools
You have access to the following functions:\n\n<tools>
""" + '\n'.join(tool_descs) + f'\n{TOOL_DESC_SUFFIX}'
if system:
tools_prompt += f'\n\n{system}'
return tools_prompt
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response>\n{tool_content}\n</tool_response>')
return '\n'.join(res_tool)
+66
View File
@@ -0,0 +1,66 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
from typing import List, Optional, Union
from .base import BaseAgentTemplate
class ReactEnAgentTemplate(BaseAgentTemplate):
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names = []
tool_descs = []
for tool in tools:
tool_desc = self._parse_tool(tool, 'en')
tool_names.append(tool_desc.name_for_model)
tool_descs.append(
f'{tool_desc.name_for_model}: Call this tool to interact with the {tool_desc.name_for_human} API. '
f'What is the {tool_desc.name_for_human} API useful for? {tool_desc.description_for_model} '
f'Parameters: {tool_desc.parameters} {tool_desc.args_format}')
return """Answer the following questions as best you can. You have access to the following tools:
""" + '\n\n'.join(tool_descs) + f"""
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{','.join(tool_names)}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can be repeated zero or more times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
"""
class ReactZnAgentTemplate(BaseAgentTemplate):
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_names = []
tool_descs = []
for tool in tools:
tool_desc = self._parse_tool(tool, 'zh')
tool_names.append(tool_desc.name_for_model)
tool_descs.append(f'{tool_desc.name_for_model}: 调用此工具与 {tool_desc.name_for_human} API 进行交互。'
f'{tool_desc.name_for_human} 有什么用?{tool_desc.description_for_model} '
f'输入参数:{tool_desc.parameters} {tool_desc.args_format}')
return """尽可能地回答以下问题。你可以使用以下工具:
""" + '\n\n'.join(tool_descs) + f"""
请按照以下格式进行:
Question: 需要你回答的输入问题
Thought: 你应该总是思考该做什么
Action: 需要使用的工具,应该是[{','.join(tool_names)}]中的一个
Action Input: 传入工具的内容
Observation: 行动的结果
... (这个Thought/Action/Action Input/Observation可以重复N次)
Thought: 我现在知道最后的答案
Final Answer: 对原始输入问题的最终答案
现在开始!
"""
+153
View File
@@ -0,0 +1,153 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import re
from typing import List, Optional, Tuple, Union
from swift.infer_engine import Function
from swift.template import Prompt
from .base import BaseAgentTemplate
class SeedAgentTemplate(BaseAgentTemplate):
TOOL_CALL_START = '<seed:tool_call>'
TOOL_CALL_END = '</seed:tool_call>'
FUNCTION_TAG = 'function'
PARAMETER_TAG = 'parameter'
_PY_TYPE_MAPPING = {
'string': 'str',
'number': 'int',
'integer': 'int',
'boolean': 'bool',
'array': 'list',
}
@staticmethod
def _py_type(t: str) -> str:
return SeedAgentTemplate._PY_TYPE_MAPPING.get(t, 'Any')
def get_toolcall(self, response: str) -> List[Function]:
res_list = re.findall(rf'{self.TOOL_CALL_START}(.+?){self.TOOL_CALL_END}', response, re.DOTALL)
if not res_list:
return super().get_toolcall(response)
functions = []
for res in res_list:
func_name_match = re.search(rf'<{self.FUNCTION_TAG}=([^>]+)>', res)
if not func_name_match:
continue
func_name = func_name_match.group(1)
param_matches = re.findall(rf'<{self.PARAMETER_TAG}=([^>]+)>(.*?)</{self.PARAMETER_TAG}>', res, re.DOTALL)
arguments = {name: value for name, value in param_matches}
functions.append(Function(name=func_name, arguments=arguments))
return functions
def _get_tool_responses(self, tool_messages: List[dict]) -> str:
responses = [f"<seed:bos>tool\n{tool_message['content']}<seed:eos>" for tool_message in tool_messages]
return ''.join(responses) + '<seed:bos>assistant\n'
def _format_tool_responses(
self,
assistant_content: str,
tool_messages: List[dict],
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
formatted_tool_responses = self._get_tool_responses(tool_messages)
return assistant_content, ['<seed:eos>', formatted_tool_responses]
def _build_tool_def_string(self, tool: dict) -> str:
"""Helper to build a single tool definition string."""
func = tool.get('function', {})
func_name = func.get('name')
if not func_name:
return ''
parameters = func.get('parameters', {})
properties = parameters.get('properties', {})
params = [
f"{name}: {self._py_type(spec.get('type', 'any'))}" for name, spec in properties.items()
if isinstance(spec, dict)
]
param_str = ','.join(params)
docstring_parts = [' """', f' {func.get("description", "").strip()}']
if properties:
docstring_parts.append('\n Args:')
required_params = parameters.get('required', [])
for name, spec in properties.items():
if isinstance(spec, dict):
req_tag = '[必填]' if name in required_params else '[选填]'
desc = spec.get('description', '')
type_str = self._py_type(spec.get('type', 'any'))
docstring_parts.append(f' - {name} ({type_str}) {req_tag}: {desc}')
returns_props = func.get('returns', {}).get('properties', {})
if returns_props:
docstring_parts.append('\n Returns:')
for name, spec in returns_props.items():
desc = spec.get('description', '')
type_str = self._py_type(spec.get('type', 'any'))
docstring_parts.append(f' - {name} ({type_str}): {desc}')
docstring_parts.append('\n """')
docstring = '\n'.join(docstring_parts)
return f'Function:\ndef {func_name}({param_str}):\n{docstring}'
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
if not tools:
return system or ''
tool_defs = [
tool_def for tool in tools if (wrapped_tool := self.wrap_tool(tool)).get('type') == 'function' and (
tool_def := self._build_tool_def_string(wrapped_tool)) != ''
]
tool_defs_joined = '\n\n'.join(tool_defs)
tool_call_format_instruction = (
'工具调用请遵循如下格式:\n'
f'{self.TOOL_CALL_START}\n'
f'<{self.FUNCTION_TAG}=example_function_name>\n'
f'<{self.PARAMETER_TAG}=example_parameter_1>value_1</{self.PARAMETER_TAG}>\n'
f'<{self.PARAMETER_TAG}=example_parameter_2>This is the value for the second parameter\n'
'that can span\n'
f'multiple lines</{self.PARAMETER_TAG}>\n'
f'</{self.FUNCTION_TAG}>\n'
f'{self.TOOL_CALL_END}')
split_token = '<seed:eos><seed:bos>system'
if system and split_token in system:
parts = system.split(split_token, 1)
return f'{parts[0]}\n\n{tool_defs_joined}\n{tool_call_format_instruction}\n{split_token}{parts[1]}'
else:
doubao_prompt = ('You are Doubao, a helpful AI assistant. '
'You may call one or more functions to assist with the user query.')
return (f'{doubao_prompt}\n\n{tool_defs_joined}\n{tool_call_format_instruction}\n'
f'{split_token}\n{system or ""}')
def _format_tool_calls(self, tool_call_messages: List[dict]) -> str:
formatted_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
func_name = tool_call['name']
arguments = tool_call.get('arguments', {})
call_parts = [f'<{self.FUNCTION_TAG}={func_name}>']
for arg_name, arg_value in arguments.items():
arg_value_str = arg_value if isinstance(arg_value, str) else json.dumps(arg_value, ensure_ascii=False)
call_parts.append(f'<{self.PARAMETER_TAG}={arg_name}>{arg_value_str}</{self.PARAMETER_TAG}>')
call_parts.append(f'</{self.FUNCTION_TAG}>')
call_parts_joined = '\n'.join(call_parts)
full_call = f'{self.TOOL_CALL_START}\n{call_parts_joined}\n{self.TOOL_CALL_END}'
formatted_calls.append(full_call)
return '\n'.join(formatted_calls)
+38
View File
@@ -0,0 +1,38 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
from typing import List, Optional, Union
from .base import BaseAgentTemplate
class ToolBenchAgentTemplate(BaseAgentTemplate):
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
for i, tool in enumerate(tools):
tools[i] = self.unwrap_tool(tool)
tools = json.dumps(tools, ensure_ascii=False)
return f"""You can use many tools(functions) to do the following task.
First I will give you the task description, and your task start.
At each step, you need to give your thought to analyze the status now and what to do next, \
with a function call to actually execute your step. Your output should follow this format:
Thought:
Action:
Action Input:
After the call, you will get the call result, and you are now in a new state.
Then you will analyze your status now, then decide what to do next...
After many (Thought-call) pairs, you finally perform the task, then you can give your final answer.
Remember:
1.the state change is irreversible, you can't go back to one of the former state, if you want to restart the task, \
say \"I give up and restart\".
2.All the thought is short, at most in 5 sentence.
3.You can do more then one try, so if your plan is to continuously try some conditions, \
you can do one of the conditions per try.
Let's Begin!
Task description: You should use functions to help handle the real time user queries. Remember:
1.ALWAYS call \"Finish\" function at the end of the task. And the final answer should contain enough information \
to show to the user,If you can't handle the task, \
or you find that function calls always fail(the function is not valid now), \
use function Finish->give_up_and_restart.
2.Do not use origin tool names, use only subfunctions' names.
Specifically, you have access to the following APIs: {tools}"""
+66
View File
@@ -0,0 +1,66 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import json
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from .hermes import HermesAgentTemplate
if TYPE_CHECKING:
from swift.template import Prompt
class YoutuAgentTemplate(HermesAgentTemplate):
"""Agent template for Youtu-LLM models.
Tool calling format:
- Tool call: <tool_call>{"name": "function-name", "arguments": {...}}</tool_call>
- Tool response: <tool_response>...</tool_response>
"""
def _get_tool_responses(self, tool_messages):
res_tool = []
for tool_message in tool_messages:
tool_content = tool_message['content']
res_tool.append(f'<tool_response>{tool_content}</tool_response>')
return '\n'.join(res_tool)
def _format_tool_responses(
self,
assistant_content: str,
tool_messages,
) -> Tuple[str, 'Prompt']:
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
if with_action:
return super()._format_tool_responses(assistant_content, tool_messages)
# For Youtu-LLM, tool responses are placed in user message
if hasattr(self, 'template_meta'):
prompt = self.template_meta.prompt
chat_sep = self.template_meta.chat_sep
else:
prompt = ['<|User|>{{QUERY}}<|Assistant|>']
chat_sep = ['<|end_of_text|>']
res = chat_sep.copy()
total_tool = self._get_tool_responses(tool_messages)
for context in prompt:
if isinstance(context, str):
context = context.replace('{{QUERY}}', total_tool)
res.append(context)
return assistant_content, res
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
system = system or ''
if system:
system = f'{system}\n\n'
return f"""{system}<|begin_of_tool_description|>Tool calling capabilities.
You may call one or more functions to assist with the user query. You have the following functions available:
""" + '\n'.join([f'```json\n{desc}\n```' for desc in tool_descs]) + """
For tool call returns, you MUST use the following format:
<tool_call>{"name": "function-name", "arguments": {"param1": "value1", "param2": "value2"}}</tool_call>
<|end_of_tool_description|>"""
def _format_tool_calls(self, tool_call_messages):
tool_calls = []
for message in tool_call_messages:
tool_call = self._parse_tool_call(message['content'])
tool_calls.append(f'<tool_call>{json.dumps(tool_call, ensure_ascii=False)}</tool_call>')
return ''.join(tool_calls)