From fc68071e1d0c2e63fd18127d24bbd811027dc729 Mon Sep 17 00:00:00 2001 From: Oleg Yermolenko Date: Fri, 28 Nov 2025 18:19:23 +0200 Subject: [PATCH] transform tool calls into proper messages --- src/lib/components/chat/Chat.svelte | 101 ++++++++++++++++++---------- src/lib/utils/index.ts | 78 +++++++++++++++++---- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 7f80bca601..e79f4b37de 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -51,7 +51,7 @@ getMessageContentParts, createMessagesList, getPromptVariables, - processDetails, + processDetailsAndExtractToolCalls, removeAllDetails, getCodeBlockContents } from '$lib/utils'; @@ -1865,46 +1865,73 @@ $settings?.params?.stream_response ?? params?.stream_response ?? true; - - let messages = [ - params?.system || $settings.system - ? { + + let messages = []; + if (params?.system || $settings.system) { + messages.push({ role: 'system', content: `${params?.system ?? $settings?.system ?? ''}` - } - : undefined, - ..._messages.map((message) => ({ - ...message, - content: processDetails(message.content) - })) - ].filter((message) => message); + }); + } - messages = messages - .map((message, idx, arr) => ({ - role: message.role, - ...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) && - message.role === 'user' - ? { - content: [ - { - type: 'text', - text: message?.merged?.content ?? message.content - }, - ...message.files - .filter((file) => file.type === 'image') - .map((file) => ({ - type: 'image_url', - image_url: { - url: file.url - } - })) - ] + for (const message of _messages) { + let content = message?.merged?.content ?? message?.content; + let processedMessages = processDetailsAndExtractToolCalls(content ?? ''); + let nonToolMesssage = null; + let toolCallIndex = 0; + + for (const processedMessage of processedMessages) { + + if (typeof processedMessage == "string") { + nonToolMesssage = { + role: message?.role, + content: message?.role === 'user' ? processedMessage : processedMessage.trim() + }; + + if (message?.role === 'user' && (message.files?.filter((file) => file.type === 'image').length > 0 ?? false)) { + nonToolMesssage = { + ...nonToolMesssage, + ...message.files + .filter((file) => file.type === 'image') + .map((file) => ({ + type: 'image_url', + image_url: { + url: file.url + } + })) } - : { - content: message?.merged?.content ?? message.content - }) - })) - .filter((message) => message?.role === 'user' || message?.content?.trim()); + } + + messages.push(nonToolMesssage); + continue; + } + + 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 toolServerIds = []; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 5b92da8b8e..2a8412424b 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -856,28 +856,78 @@ export const removeAllDetails = (content) => { return content; }; -export const processDetails = (content) => { - content = removeDetails(content, ['reasoning', 'code_interpreter']); +// This regex matches
tags with type="tool_calls" and captures their attributes +const toolCallsDetailsRegex = /]*)>([\s\S]*?)<\/details>/gis; +const detailsAttributesRegex = /(\w+)="([^"]*)"/g; - // This regex matches
tags with type="tool_calls" and captures their attributes to convert them to a string - const detailsRegex = /]*)>([\s\S]*?)<\/details>/gis; - const matches = content.match(detailsRegex); - if (matches) { +export const processDetailsAndExtractToolCalls = (content) => { + content = removeDetails(content, ['reasoning', 'code_interpreter']); + + // 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) { - const attributesRegex = /(\w+)="([^"]*)"/g; - const attributes = {}; - let attributeMatch; - while ((attributeMatch = attributesRegex.exec(match)) !== null) { - attributes[attributeMatch[1]] = attributeMatch[2]; + + let detailsStartIndex = content.indexOf(match, previousDetailsEndIndex); + let assistantMessage = content.substr(previousDetailsEndIndex, detailsStartIndex - previousDetailsEndIndex); + previousDetailsEndIndex = detailsStartIndex + match.length; + + 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); } } - - return content; + else if (content.length > 0) { + 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 const codeBlockRegex = /```[\s\S]*?```/g;