Skip to content

Commit 87ccab3

Browse files
authored
fix chat template with tool call (#3773)
1 parent 75aa7e9 commit 87ccab3

File tree

2 files changed

+164
-20
lines changed

2 files changed

+164
-20
lines changed

lmdeploy/model.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,18 +1965,20 @@ def match(cls, model_path: str) -> Optional[str]:
19651965
return 'llama4'
19661966

19671967

1968+
@MODELS.register_module(name='intern-s1')
19681969
@MODELS.register_module(name='interns1')
19691970
class InternS1(InternVL2_5):
19701971

19711972
def __init__(
19721973
self,
1973-
tool='# External Tools\nYou have access to these tools:\n',
1974-
eotool='\n# Tool Call Formatted\nYour response should consist of a reasoning step (**thought**) followed immediately by a function call in valid JSON format. Wrap each function call using the `<|action_start|><|plugin|>` and `<|action_end|>` tags.\n**Format example:**\n```\n(Your thought goes here...)\n<|action_start|><|plugin|>\n{\n "name": "tool_name",\n "parameters": {\n "parameter1": "value1",\n "parameter2": "value2"\n }\n}\n<|action_end|>\n```', # noqa: E501
1974+
tool='\n\nYour response should consist of a reasoning step (**thought**) followed immediately by a function call in valid JSON format. Wrap each function call using the `<|action_start|><|plugin|>` and `<|action_end|>` tags.\n\n**Format example:**\n\n```\n(Your thought goes here...)\n\n<|action_start|><|plugin|>\n{\n "name": "tool_name",\n "parameters": {\n "parameter1": "value1",\n "parameter2": "value2"\n }\n}\n<|action_end|>\n```\n\n# External Tools\nYou have access to these tools:\n', # noqa: E501
1975+
eotool='',
19751976
meta_instruction='You are an expert reasoner with extensive experience in all areas. You approach problems through systematic thinking and rigorous reasoning. Your response should reflect deep understanding and precise logical thinking, making your solution path and reasoning clear to others. Please put your thinking process within <think>...</think> tags.', # noqa: E501
19761977
**kwargs):
1978+
super(InternVL2_5, self).__init__(meta_instruction=meta_instruction, **kwargs)
1979+
19771980
self.tool = tool or ''
19781981
self.eotool = eotool or ''
1979-
super(InternVL2_5, self).__init__(meta_instruction=meta_instruction, **kwargs)
19801982

19811983
def messages2prompt(self, messages, sequence_start=True, tools=None, enable_thinking=None, **kwargs):
19821984
"""Return the prompt that is concatenated with other elements in the
@@ -2000,24 +2002,40 @@ def messages2prompt(self, messages, sequence_start=True, tools=None, enable_thin
20002002
environment=self.eoenv,
20012003
tool=self.eoenv)
20022004
name_map = dict(plugin=self.plugin, interpreter=self.interpreter)
2005+
20032006
ret = ''
2004-
if self.meta_instruction is not None and sequence_start:
2005-
if len(messages):
2006-
if messages[0]['role'] != 'system' and enable_thinking is not False:
2007-
ret += f'{self.system}{self.meta_instruction}{eox_map["system"]}'
20082007

20092008
if tools:
20102009
tools_prompt = dict(
20112010
role='system',
20122011
name='plugin', # only support internlm2
2013-
content=f'{self.tool}{json.dumps(tools, ensure_ascii=False)}{self.eotool}')
2014-
insert_index = 0
2012+
content=f'{self.tool}{json.dumps(tools, ensure_ascii=False, indent=2)}{self.eotool}')
2013+
20152014
if messages[0]['role'] == 'system':
2016-
insert_index = 1
2017-
messages.insert(insert_index, tools_prompt)
2018-
for message in messages:
2015+
tools_prompt['content'] = messages[0]['content'] + tools_prompt['content']
2016+
messages[0] = tools_prompt
2017+
else:
2018+
if self.meta_instruction is not None and sequence_start and enable_thinking is not False:
2019+
tools_prompt['content'] = self.meta_instruction + tools_prompt['content']
2020+
else:
2021+
tools_prompt['content'] = tools_prompt['content'].lstrip('\n')
2022+
messages.insert(0, tools_prompt)
2023+
elif self.meta_instruction is not None and sequence_start:
2024+
if len(messages):
2025+
if messages[0]['role'] != 'system' and enable_thinking is not False:
2026+
ret += f'{self.system}{self.meta_instruction}{eox_map["system"]}'
2027+
# find index of last user input section
2028+
last_user_idx = -1
2029+
for idx in range(len(messages) - 1, -1, -1):
2030+
if messages[idx]['role'] == 'user':
2031+
last_user_idx = idx
2032+
break
2033+
2034+
for idx, message in enumerate(messages):
20192035
role = message['role']
20202036
content = get_text(message['content'])
2037+
if last_user_idx != -1 and idx > last_user_idx and message.get('reasoning_content', None) is not None:
2038+
content = f'<think>\n{message["reasoning_content"]}\n</think>\n{content}'
20212039
if role == 'assistant' and message.get('tool_calls', None) is not None:
20222040
for tool_call in message['tool_calls']:
20232041
function = tool_call.get('function', {})
@@ -2026,9 +2044,14 @@ def messages2prompt(self, messages, sequence_start=True, tools=None, enable_thin
20262044
function.pop('arguments')
20272045
if isinstance(function['parameters'], str):
20282046
function['parameters'] = json.loads(function['parameters'])
2029-
content += f'<|action_start|><|plugin|>\n{json.dumps(function, ensure_ascii=False)}<|action_end|>'
2030-
if 'name' in message and message['name'] in name_map:
2031-
begin = box_map[role].strip() + f" name={name_map[message['name']]}\n"
2047+
content += f'<|action_start|><|plugin|>\n{json.dumps(function, ensure_ascii=False)}\n<|action_end|>'
2048+
2049+
if 'name' in message:
2050+
begin = box_map[role].strip()
2051+
if message['name'] in name_map:
2052+
begin = begin + f" name={name_map[message['name']]}\n"
2053+
elif role == 'tool':
2054+
begin = begin + f" name={name_map['plugin']}\n"
20322055
else:
20332056
begin = box_map[role]
20342057
ret += f'{begin}{content}{eox_map[role]}'
@@ -2048,8 +2071,8 @@ def match(cls, model_path: str) -> Optional[str]:
20482071
model_path (str): the model path used for matching.
20492072
"""
20502073
path = model_path.lower()
2051-
if 'interns1' in path:
2052-
return 'interns1'
2074+
if 'intern-s1' in path or 'interns1' in path:
2075+
return 'intern-s1'
20532076

20542077

20552078
def best_match_model(query: str) -> Optional[str]:

tests/test_lmdeploy/test_model.py

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,13 +1008,14 @@ def test_qwen3(model_path, enable_thinking):
10081008

10091009

10101010
@pytest.mark.parametrize('model_path', ['internlm/Intern-S1'])
1011-
@pytest.mark.parametrize('enable_thinking', [True, False, None])
1012-
def test_interns1(model_path, enable_thinking):
1011+
@pytest.mark.parametrize('enable_thinking', [None, True, False])
1012+
@pytest.mark.parametrize('has_user_sys', [True, False])
1013+
def test_interns1(model_path, enable_thinking, has_user_sys):
10131014
from transformers import AutoTokenizer
10141015
try:
10151016
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
10161017
except OSError:
1017-
pytest.skip(reason='internlm/Intern-S1 not exists')
1018+
pytest.skip(reason=f'{model_path} not exists')
10181019

10191020
chat_template_name = best_match_model(model_path)
10201021
chat_template = MODELS.get(chat_template_name)()
@@ -1032,6 +1033,9 @@ def test_interns1(model_path, enable_thinking):
10321033
'role': 'user',
10331034
'content': 'AGI is?'
10341035
}]
1036+
if not has_user_sys:
1037+
messages = messages[1:]
1038+
10351039
if enable_thinking is None:
10361040
ref = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
10371041
else:
@@ -1041,3 +1045,120 @@ def test_interns1(model_path, enable_thinking):
10411045
enable_thinking=enable_thinking)
10421046
lm_res = chat_template.messages2prompt(messages, enable_thinking=enable_thinking)
10431047
assert ref == lm_res
1048+
1049+
1050+
@pytest.mark.parametrize('model_path', ['internlm/Intern-S1'])
1051+
@pytest.mark.parametrize('enable_thinking', [None, True, False])
1052+
@pytest.mark.parametrize('has_user_sys', [True, False])
1053+
def test_interns1_tools(model_path, enable_thinking, has_user_sys):
1054+
from transformers import AutoTokenizer
1055+
try:
1056+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
1057+
except OSError:
1058+
pytest.skip(reason=f'{model_path} not exists')
1059+
1060+
chat_template_name = best_match_model(model_path)
1061+
chat_template = MODELS.get(chat_template_name)()
1062+
1063+
tools = [
1064+
{
1065+
'type': 'function',
1066+
'function': {
1067+
'name': 'find_user_id_by_name_zip',
1068+
'description':
1069+
'Find user id by first name, last name, and zip code. If the user is not found, the function will return an error message. By default, find user id by email, and only call this function if the user is not found by email or cannot remember email.', # noqa: E501
1070+
'parameters': {
1071+
'type': 'object',
1072+
'properties': {
1073+
'first_name': {
1074+
'type': 'string',
1075+
'description': "The first name of the customer, such as 'John'."
1076+
},
1077+
'last_name': {
1078+
'type': 'string',
1079+
'description': "The last name of the customer, such as 'Doe'."
1080+
},
1081+
'zip': {
1082+
'type': 'string',
1083+
'description': "The zip code of the customer, such as '12345'."
1084+
}
1085+
},
1086+
'required': ['first_name', 'last_name', 'zip']
1087+
}
1088+
}
1089+
},
1090+
{
1091+
'type': 'function',
1092+
'function': {
1093+
'name': 'get_order_details',
1094+
'description': 'Get the status and details of an order.',
1095+
'parameters': {
1096+
'type': 'object',
1097+
'properties': {
1098+
'order_id': {
1099+
'type':
1100+
'string',
1101+
'description':
1102+
"The order id, such as '#W0000000'. Be careful there is a '#' symbol at the beginning of the order id." # noqa: E501
1103+
}
1104+
},
1105+
'required': ['order_id']
1106+
}
1107+
}
1108+
}
1109+
]
1110+
messages = [
1111+
{
1112+
'role': 'system',
1113+
'content': 'You are a helpful assistant'
1114+
},
1115+
{
1116+
'role': 'user',
1117+
'content': "Hi there! I'm looking to return a couple of items from a recent order."
1118+
},
1119+
{
1120+
'role':
1121+
'assistant',
1122+
'content':
1123+
'Could you please provide your email address associated with the account, or share your first name, last name, and zip code?', # noqa: E501
1124+
'reasoning_content':
1125+
'Okay, the user wants to return some items from a recent order. Let me start by authenticating their identity...' # noqa: E501
1126+
},
1127+
{
1128+
'role': 'user',
1129+
'content': 'Sure, my name is Omar Anderson and my zip code is 19031.'
1130+
},
1131+
{
1132+
'role':
1133+
'assistant',
1134+
'content':
1135+
'<content>',
1136+
'reasoning_content':
1137+
"Since he didn't provide an email, I should use the find_user_id_by_name_zip function. Let me...", # noqa: E501
1138+
'tool_calls': [{
1139+
'function': {
1140+
'arguments': '{"first_name": "Omar", "last_name": "Anderson", "zip": "19031"}',
1141+
'name': 'find_user_id_by_name_zip'
1142+
},
1143+
'id': 'chatcmpl-tool-a9f439084bfc4af29fee2e5105050a38',
1144+
'type': 'function'
1145+
}]
1146+
},
1147+
{
1148+
'content': 'omar_anderson_3203',
1149+
'name': 'find_user_id_by_name_zip',
1150+
'role': 'tool'
1151+
}
1152+
]
1153+
if not has_user_sys:
1154+
messages = messages[1:]
1155+
if enable_thinking is None:
1156+
ref = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools)
1157+
else:
1158+
ref = tokenizer.apply_chat_template(messages,
1159+
tokenize=False,
1160+
add_generation_prompt=True,
1161+
tools=tools,
1162+
enable_thinking=enable_thinking)
1163+
lm_res = chat_template.messages2prompt(messages, enable_thinking=enable_thinking, tools=tools)
1164+
assert ref == lm_res

0 commit comments

Comments
 (0)