diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index d49a79b3b1..badadf0b40 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -374,6 +374,21 @@ ENABLE_REALTIME_CHAT_SAVE = ( ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true" +ENABLE_WRAP_TOOL_RESULT = ( + os.environ.get("ENABLE_WRAP_TOOL_RESULT", "True").lower() == "true" +) + +TOOL_RESULT_INDENT_SIZE = os.environ.get("TOOL_RESULT_INDENT_SIZE", 2) + +if TOOL_RESULT_INDENT_SIZE == "": + TOOL_RESULT_INDENT_SIZE = 2 +else: + try: + TOOL_RESULT_INDENT_SIZE = int(TOOL_RESULT_INDENT_SIZE) + except Exception: + TOOL_RESULT_INDENT_SIZE = 2 + + #################################### # REDIS #################################### diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index d397471dd9..d6f1e1a255 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -118,6 +118,8 @@ from open_webui.env import ( BYPASS_MODEL_ACCESS_CONTROL, ENABLE_REALTIME_CHAT_SAVE, ENABLE_QUERIES_CACHE, + ENABLE_WRAP_TOOL_RESULT, + TOOL_RESULT_INDENT_SIZE, ) from open_webui.constants import TASKS @@ -275,15 +277,23 @@ def process_tool_result( ) tool_result.remove(item) - if isinstance(tool_result, list): + if isinstance(tool_result, list) and ENABLE_WRAP_TOOL_RESULT: tool_result = {"results": tool_result} if isinstance(tool_result, dict) or isinstance(tool_result, list): - tool_result = json.dumps(tool_result, indent=2, ensure_ascii=False) + tool_result = dump_tool_result_to_json(tool_result, ensure_ascii=False) return tool_result, tool_result_files, tool_result_embeds +def dump_tool_result_to_json(model, ensure_ascii=True): + indent_size = None if TOOL_RESULT_INDENT_SIZE == 0 else TOOL_RESULT_INDENT_SIZE + separators = None if indent_size and indent_size > 0 else (",", ":") + return json.dumps( + model, indent=indent_size, separators=separators, ensure_ascii=ensure_ascii + ) + + async def chat_completion_tools_handler( request: Request, body: dict, extra_params: dict, user: UserModel, models, tools ) -> tuple[dict, dict]: @@ -2070,9 +2080,9 @@ async def process_chat_response( if tool_result is not None: tool_result_embeds = result.get("embeds", "") - tool_calls_display_content = f'{tool_calls_display_content}
\nTool Executed\n
\n' + tool_calls_display_content = f'{tool_calls_display_content}
\nTool Executed\n
\n' else: - tool_calls_display_content = f'{tool_calls_display_content}
\nExecuting...\n
\n' + tool_calls_display_content = f'{tool_calls_display_content}
\nExecuting...\n
\n' if not raw: content = f"{content}{tool_calls_display_content}" @@ -2088,7 +2098,7 @@ async def process_chat_response( "arguments", "" ) - tool_calls_display_content = f'{tool_calls_display_content}\n
\nExecuting...\n
\n' + tool_calls_display_content = f'{tool_calls_display_content}\n
\nExecuting...\n
\n' if not raw: content = f"{content}{tool_calls_display_content}" diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index c2ebe59bea..c82e645586 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'; @@ -1873,45 +1873,79 @@ params?.stream_response ?? true; - let messages = [ - params?.system || $settings.system - ? { - role: 'system', - content: `${params?.system ?? $settings?.system ?? ''}` - } - : undefined, - ..._messages.map((message) => ({ - ...message, - content: processDetails(message.content) - })) - ].filter((message) => message); + let messages = []; + if (params?.system || $settings.system) { + messages.push({ + role: 'system', + content: `${params?.system ?? $settings?.system ?? ''}` + }); + } - 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 - } - })) - ] - } - : { - content: message?.merged?.content ?? message.content - }) - })) - .filter((message) => message?.role === 'user' || message?.content?.trim()); + for (const message of _messages) { + let content = message?.merged?.content ?? message?.content; + content = message?.role !== 'user' ? content?.trim() : 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: processedMessage + }; + + if ( + message?.role === 'user' && + (message.files?.filter((file) => file.type === 'image').length > 0 ?? false) + ) { + nonToolMesssage.content = [ + { + type: 'text', + text: nonToolMesssage.content + }, + ...message.files + .filter((file) => file.type === 'image') + .map((file) => ({ + type: 'image_url', + image_url: { + url: file.url + } + })) + ]; + } + + 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..feffbd546b 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -856,28 +856,77 @@ export const removeAllDetails = (content) => { return content; }; -export const processDetails = (content) => { +// This regex matches
tags with type="tool_calls" and captures their attributes +const toolCallsDetailsRegex = /]*)>([\s\S]*?)<\/details>/gis; +const detailsAttributesRegex = /(\w+)="([^"]*)"/g; + +export const processDetailsAndExtractToolCalls = (content) => { content = removeDetails(content, ['reasoning', 'code_interpreter']); - // 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) { + // 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; + 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); + } + const attributes = {}; let attributeMatch; - while ((attributeMatch = attributesRegex.exec(match)) !== null) { + while ((attributeMatch = detailsAttributesRegex.exec(match)) !== null) { attributes[attributeMatch[1]] = attributeMatch[2]; } - content = content.replace(match, `"${attributes.result}"`); + 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) { + messages.push(content); } - return content; + return messages; }; +function parseDoubleEncodedString(value) { + try { + let parsedValue = JSON.parse(value); + if (typeof parsedValue == 'string') { + return parsedValue; + } + } catch {} + + return value; +} + // This regular expression matches code blocks marked by triple backticks const codeBlockRegex = /```[\s\S]*?```/g;