Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/open_webui/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,21 @@ def parse_section(section):

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
####################################
Expand Down
20 changes: 15 additions & 5 deletions backend/open_webui/utils/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@
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

Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -2060,9 +2070,9 @@ def serialize_content_blocks(content_blocks, raw=False):

if tool_result is not None:
tool_result_embeds = result.get("embeds", "")
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result, ensure_ascii=False))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}" embeds="{html.escape(json.dumps(tool_result_embeds))}">\n<summary>Tool Executed</summary>\n</details>\n'
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(dump_tool_result_to_json(tool_arguments))}" result="{html.escape(dump_tool_result_to_json(tool_result, ensure_ascii=True))}" files="{html.escape(dump_tool_result_to_json(tool_result_files)) if tool_result_files else ""}" embeds="{html.escape(dump_tool_result_to_json(tool_result_embeds))}">\n<summary>Tool Executed</summary>\n</details>\n'
else:
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(dump_tool_result_to_json(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'

if not raw:
content = f"{content}{tool_calls_display_content}"
Expand All @@ -2078,7 +2088,7 @@ def serialize_content_blocks(content_blocks, raw=False):
"arguments", ""
)

tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(dump_tool_result_to_json(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'

if not raw:
content = f"{content}{tool_calls_display_content}"
Expand Down
112 changes: 73 additions & 39 deletions src/lib/components/chat/Chat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
getMessageContentParts,
createMessagesList,
getPromptVariables,
processDetails,
processDetailsAndExtractToolCalls,
removeAllDetails,
getCodeBlockContents
} from '$lib/utils';
Expand Down Expand Up @@ -1866,45 +1866,79 @@
params?.stream_response ??
true;

let messages = [
params?.system || $settings.system
? {
role: 'system',
content: `${params?.system ?? $settings?.system ?? ''}`
let messages = [];
if (params?.system || $settings.system) {
messages.push({
role: 'system',
content: `${params?.system ?? $settings?.system ?? ''}`
});
}

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
}
}))
];
}
: 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
}
}))
]
}
: {
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 = [];
Expand Down
67 changes: 58 additions & 9 deletions src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,28 +856,77 @@ export const removeAllDetails = (content) => {
return content;
};

export const processDetails = (content) => {
// This regex matches <details> tags with type="tool_calls" and captures their attributes
const toolCallsDetailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis;
const detailsAttributesRegex = /(\w+)="([^"]*)"/g;

export const processDetailsAndExtractToolCalls = (content) => {
content = removeDetails(content, ['reasoning', 'code_interpreter']);

// This regex matches <details> tags with type="tool_calls" and captures their attributes to convert them to a string
const detailsRegex = /<details\s+type="tool_calls"([^>]*)>([\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;

Expand Down