This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Copyright (c) ModelScope Contributors. All rights reserved.
|
||||
from .base import BaseAgentTemplate
|
||||
from .mapping import agent_template_map
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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: <|DSML|invoke name="tool_name">...params...</|DSML|invoke>
|
||||
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>'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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>"""
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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\|>'
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 ''
|
||||
@@ -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}'
|
||||
@@ -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
|
||||
@@ -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 """ # 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✿: 根据工具结果进行回复,需将图片用渲染出来""" # 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 """ # 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✿: 根据工具结果进行回复,需将图片用渲染出来""" # noqa
|
||||
@@ -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)
|
||||
@@ -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: 对原始输入问题的最终答案
|
||||
|
||||
现在开始!
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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}"""
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user