131
request: ChatCompletionRequest,
132
) -> ExtractedToolCallInformation:
133
"""
134
- Extract the tool calls from a complete model response. Requires
135
- find-and-replacing single quotes with double quotes for JSON parsing,
136
- make sure your tool call arguments don't ever include quotes!
134
+ Extract the tool calls from a complete model response.
135
+
136
+ Content and tool calls formatting depends on the Mistral's tokenizer version
137
+ used to train the model:
138
+
139
+ - < v11: `content[BOT] [{tool_call1},{tool_call2}]`
140
+ - >= v11: `content[BOT]tool_name1{args_call1}[BOT]tool_name2{args_call2}`
141
+
142
+ with [BOT] the tool call token.
143
+
144
+ Note:
145
+ For tokenizer versions >= v11, tool calls with arguments wrongly formatted
146
+ are still returned as tool calls. This is to allow the model to know it
147
+ tried to make a tool call. It reduces chance of another failure and
148
+ prevents that the context is filled with tool calls wrongly placed in
149
+ assistant message contents.
150
"""
151
139
- # case -- if a tool call token is not present, return a text response
152
+ # If the tool call token is not present, return a text response
153
if self.bot_token not in model_output:
154
return ExtractedToolCallInformation(
155
tools_called=False, tool_calls=[], content=model_output
156
)
157
145
- # first remove the BOT token
146
- tool_content = model_output.replace(self.bot_token, "").strip()
158
+ content_and_raw_tool_calls = model_output.split(self.bot_token)
159
+ content = content_and_raw_tool_calls[0]
160
+ raw_tool_calls = content_and_raw_tool_calls[1:]
161
+
162
+ # >= v11: content[BOT]tool_name1{args_call1}[BOT]tool_name2{args_call2}
163
+ if not self._is_pre_v11:
164
+ tool_calls = []
165
+ for raw_tool_call in raw_tool_calls:
166
+ if "{" not in raw_tool_call:
167
+ continue
168
+
169
+ end_name = raw_tool_call.find("{")
170
+ tool_name, args = (
171
+ raw_tool_call[:end_name],
172
+ raw_tool_call[end_name:],
173
+ )
174
148
- try:
175
+ tool_calls.append({"name": tool_name, "arguments": args})
176
+
177
+ # < v11: content[BOT] [{tool_call1},{tool_call2}]
178
+ else:
179
+ if len(raw_tool_calls) != 1:
180
+ raise ValueError(
181
+ "Only one BOT token should have been outputted, "
182
+ f"but got {model_output}."
183
+ )
184
+ stringified_tool_calls = raw_tool_calls[0].strip()
185
try:
150
- if not self._is_pre_v11:
151
- function_call_arr = []
152
- for single_tool_content in model_output.split(self.bot_token):
153
- if "{" not in single_tool_content:
154
- continue
155
-
156
- end_name = single_tool_content.find("{")
157
- fn_name, args = (
158
- single_tool_content[:end_name],
159
- single_tool_content[end_name:],
160
- )
161
-
162
- # fn_name is encoded outside serialized json dump
163
- # only arguments are serialized
164
- function_call_arr.append(
165
- {"name": fn_name, "arguments": json.loads(args)}
166
- )
167
- else:
168
- function_call_arr = json.loads(tool_content)
186
+ tool_calls = json.loads(stringified_tool_calls)
187
except json.JSONDecodeError:
188
# use a regex to find the part corresponding to the tool call.
189
# NOTE: This use case should not happen if the model is trained
190
# correctly. It's an easy possible fix so it's included, but
191
# can be brittle for very complex / highly nested tool calls
174
- raw_tool_call = self.tool_call_regex.findall(tool_content)[0]
175
- function_call_arr = json.loads(raw_tool_call)
176
-
177
- # Tool Call
178
- tool_calls: list[MistralToolCall] = [
179
- MistralToolCall(
180
- type="function",
181
- function=FunctionCall(
182
- name=raw_function_call["name"],
183
- # function call args are JSON but as a string
184
- arguments=json.dumps(
185
- raw_function_call["arguments"], ensure_ascii=False
192
+ try:
193
+ raw_tool_call = self.tool_call_regex.findall(
194
+ stringified_tool_calls
195
+ )[0]
196
+ tool_calls = json.loads(raw_tool_call)
197
+ except (IndexError, json.JSONDecodeError):
198
+ logger.exception("Error in extracting tool call from response: {e}")
199
+ # If raw decoding and decoding post regex rule fails, then just
200
+ # return content.
201
+ return ExtractedToolCallInformation(
202
+ tools_called=False,
203
+ tool_calls=[],
204
+ content=stringified_tool_calls,
205
+ )
206
+ else:
207
+ tool_calls = [
208
+ {
209
+ "name": tool_call["name"],
210
+ "arguments": json.dumps(
211
+ tool_call["arguments"], ensure_ascii=False
212
),
187
- ),
188
- )
189
- for raw_function_call in function_call_arr
190
- ]
213
+ }
214
+ for tool_call in tool_calls
215
+ ]
216
192
- # get any content before the tool call
193
- content = model_output.split(self.bot_token)[0]
194
- return ExtractedToolCallInformation(
195
- tools_called=True,
196
- tool_calls=tool_calls,
197
- content=content if len(content) > 0 else None,
217
+ mistral_tool_calls: list[MistralToolCall] = [
218
+ MistralToolCall(
219
+ type="function",
220
+ function=FunctionCall(
221
+ name=tool_call["name"],
222
+ arguments=tool_call["arguments"],
223
+ ),
224
)
225
+ for tool_call in tool_calls
226
+ ]
227
200
- except Exception:
201
- logger.exception("Error in extracting tool call from response.")
202
- # return information to just treat the tool call as regular JSON
203
- return ExtractedToolCallInformation(
204
- tools_called=False, tool_calls=[], content=tool_content
205
- )
228
+ return ExtractedToolCallInformation(
229
+ tools_called=True,
230
+ tool_calls=mistral_tool_calls,
231
+ content=content if len(content) > 0 else None,
232
+ )
233
234
def extract_tool_calls_streaming(
235
self,