transform tool calls into proper messages

This commit is contained in:
Oleg Yermolenko 2025-11-28 18:19:23 +02:00
parent 453ea9b9a1
commit fc68071e1d
2 changed files with 128 additions and 51 deletions

View file

@ -51,7 +51,7 @@
getMessageContentParts, getMessageContentParts,
createMessagesList, createMessagesList,
getPromptVariables, getPromptVariables,
processDetails, processDetailsAndExtractToolCalls,
removeAllDetails, removeAllDetails,
getCodeBlockContents getCodeBlockContents
} from '$lib/utils'; } from '$lib/utils';
@ -1865,46 +1865,73 @@
$settings?.params?.stream_response ?? $settings?.params?.stream_response ??
params?.stream_response ?? params?.stream_response ??
true; true;
let messages = [ let messages = [];
params?.system || $settings.system if (params?.system || $settings.system) {
? { messages.push({
role: 'system', role: 'system',
content: `${params?.system ?? $settings?.system ?? ''}` content: `${params?.system ?? $settings?.system ?? ''}`
} });
: undefined, }
..._messages.map((message) => ({
...message,
content: processDetails(message.content)
}))
].filter((message) => message);
messages = messages for (const message of _messages) {
.map((message, idx, arr) => ({ let content = message?.merged?.content ?? message?.content;
role: message.role, let processedMessages = processDetailsAndExtractToolCalls(content ?? '');
...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) && let nonToolMesssage = null;
message.role === 'user' let toolCallIndex = 0;
? {
content: [ for (const processedMessage of processedMessages) {
{
type: 'text', if (typeof processedMessage == "string") {
text: message?.merged?.content ?? message.content nonToolMesssage = {
}, role: message?.role,
...message.files content: message?.role === 'user' ? processedMessage : processedMessage.trim()
.filter((file) => file.type === 'image') };
.map((file) => ({
type: 'image_url', if (message?.role === 'user' && (message.files?.filter((file) => file.type === 'image').length > 0 ?? false)) {
image_url: { nonToolMesssage = {
url: file.url ...nonToolMesssage,
} ...message.files
})) .filter((file) => file.type === 'image')
] .map((file) => ({
type: 'image_url',
image_url: {
url: file.url
}
}))
} }
: { }
content: message?.merged?.content ?? message.content
}) messages.push(nonToolMesssage);
})) continue;
.filter((message) => message?.role === 'user' || message?.content?.trim()); }
if (!nonToolMesssage) {
nonToolMesssage = {
role: message?.role,
content: ''
};
messages.push(nonToolMesssage);
}
nonToolMesssage.tool_calls ??= [];
nonToolMesssage.tool_calls.push({
index: toolCallIndex++,
id: processedMessage.id,
type: 'function',
function: {
name: processedMessage.name,
arguments: processedMessage.arguments
}
});
messages.push({
role: 'tool',
tool_call_id: processedMessage.id,
content: processedMessage.result
});
}
}
const toolIds = []; const toolIds = [];
const toolServerIds = []; const toolServerIds = [];

View file

@ -856,28 +856,78 @@ export const removeAllDetails = (content) => {
return content; return content;
}; };
export const processDetails = (content) => { // This regex matches <details> tags with type="tool_calls" and captures their attributes
content = removeDetails(content, ['reasoning', 'code_interpreter']); const toolCallsDetailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis;
const detailsAttributesRegex = /(\w+)="([^"]*)"/g;
// This regex matches <details> tags with type="tool_calls" and captures their attributes to convert them to a string export const processDetailsAndExtractToolCalls = (content) => {
const detailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis; content = removeDetails(content, ['reasoning', 'code_interpreter']);
const matches = content.match(detailsRegex);
if (matches) { // Split text and tool calls into messages array
let messages = [];
const matches = content.match(toolCallsDetailsRegex);
if (matches && matches.length > 0) {
let previousDetailsEndIndex = 0;
for (const match of matches) { for (const match of matches) {
const attributesRegex = /(\w+)="([^"]*)"/g;
const attributes = {}; let detailsStartIndex = content.indexOf(match, previousDetailsEndIndex);
let attributeMatch; let assistantMessage = content.substr(previousDetailsEndIndex, detailsStartIndex - previousDetailsEndIndex);
while ((attributeMatch = attributesRegex.exec(match)) !== null) { previousDetailsEndIndex = detailsStartIndex + match.length;
attributes[attributeMatch[1]] = attributeMatch[2];
assistantMessage = assistantMessage.trim('\n');
if (assistantMessage.length > 0) {
messages.push(assistantMessage);
} }
content = content.replace(match, `"${attributes.result}"`); const attributes = {};
let attributeMatch;
while ((attributeMatch = detailsAttributesRegex.exec(match)) !== null) {
attributes[attributeMatch[1]] = attributeMatch[2];
}
if (!attributes.id) {
continue;
}
let toolCall = {
id: attributes.id,
name: attributes.name,
arguments: unescapeHtml(attributes.arguments ?? ''),
result: unescapeHtml(attributes.result ?? '')
}
toolCall.arguments = parseDoubleEncodedString(toolCall.arguments);
toolCall.result = parseDoubleEncodedString(toolCall.result);
messages.push(toolCall);
}
let finalAssistantMessage = content.substr(previousDetailsEndIndex);
finalAssistantMessage = finalAssistantMessage.trim('\n');
if (finalAssistantMessage.length > 0) {
messages.push(finalAssistantMessage);
} }
} }
else if (content.length > 0) {
return content; messages.push(content);
}
return messages;
}; };
function parseDoubleEncodedString(value) {
try
{
let parsedValue = JSON.parse(value);
if (typeof value == "string") {
return parsedValue;
}
}
catch {}
return value;
}
// This regular expression matches code blocks marked by triple backticks // This regular expression matches code blocks marked by triple backticks
const codeBlockRegex = /```[\s\S]*?```/g; const codeBlockRegex = /```[\s\S]*?```/g;