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 1d5abcb030..84fb418d08 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;