This commit is contained in:
Oleg Yermolenko 2025-12-11 16:10:14 -03:00 committed by GitHub
commit 045bb3e185
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 161 additions and 53 deletions

View file

@ -374,6 +374,21 @@ ENABLE_REALTIME_CHAT_SAVE = (
ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true" 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 # REDIS
#################################### ####################################

View file

@ -118,6 +118,8 @@ from open_webui.env import (
BYPASS_MODEL_ACCESS_CONTROL, BYPASS_MODEL_ACCESS_CONTROL,
ENABLE_REALTIME_CHAT_SAVE, ENABLE_REALTIME_CHAT_SAVE,
ENABLE_QUERIES_CACHE, ENABLE_QUERIES_CACHE,
ENABLE_WRAP_TOOL_RESULT,
TOOL_RESULT_INDENT_SIZE,
) )
from open_webui.constants import TASKS from open_webui.constants import TASKS
@ -275,15 +277,23 @@ def process_tool_result(
) )
tool_result.remove(item) tool_result.remove(item)
if isinstance(tool_result, list): if isinstance(tool_result, list) and ENABLE_WRAP_TOOL_RESULT:
tool_result = {"results": tool_result} tool_result = {"results": tool_result}
if isinstance(tool_result, dict) or isinstance(tool_result, list): 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 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( async def chat_completion_tools_handler(
request: Request, body: dict, extra_params: dict, user: UserModel, models, tools request: Request, body: dict, extra_params: dict, user: UserModel, models, tools
) -> tuple[dict, dict]: ) -> tuple[dict, dict]:
@ -2070,9 +2080,9 @@ async def process_chat_response(
if tool_result is not None: if tool_result is not None:
tool_result_embeds = result.get("embeds", "") 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: 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: if not raw:
content = f"{content}{tool_calls_display_content}" content = f"{content}{tool_calls_display_content}"
@ -2088,7 +2098,7 @@ async def process_chat_response(
"arguments", "" "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: if not raw:
content = f"{content}{tool_calls_display_content}" content = f"{content}{tool_calls_display_content}"

View file

@ -51,7 +51,7 @@
getMessageContentParts, getMessageContentParts,
createMessagesList, createMessagesList,
getPromptVariables, getPromptVariables,
processDetails, processDetailsAndExtractToolCalls,
removeAllDetails, removeAllDetails,
getCodeBlockContents getCodeBlockContents
} from '$lib/utils'; } from '$lib/utils';
@ -1873,29 +1873,37 @@
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, content = message?.role !== 'user' ? content?.trim() : content;
...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) && let processedMessages = processDetailsAndExtractToolCalls(content ?? '');
message.role === 'user'
? { let nonToolMesssage = null;
content: [ 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', type: 'text',
text: message?.merged?.content ?? message.content text: nonToolMesssage.content
}, },
...message.files ...message.files
.filter((file) => file.type === 'image') .filter((file) => file.type === 'image')
@ -1905,13 +1913,39 @@
url: file.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
});
}
} }
: {
content: message?.merged?.content ?? message.content
})
}))
.filter((message) => message?.role === 'user' || message?.content?.trim());
const toolIds = []; const toolIds = [];
const toolServerIds = []; const toolServerIds = [];

View file

@ -856,28 +856,77 @@ 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
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']); 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 // Split text and tool calls into messages array
const detailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis; let messages = [];
const matches = content.match(detailsRegex); const matches = content.match(toolCallsDetailsRegex);
if (matches) { if (matches && matches.length > 0) {
let previousDetailsEndIndex = 0;
for (const match of matches) { 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 = {}; const attributes = {};
let attributeMatch; let attributeMatch;
while ((attributeMatch = attributesRegex.exec(match)) !== null) { while ((attributeMatch = detailsAttributesRegex.exec(match)) !== null) {
attributes[attributeMatch[1]] = attributeMatch[2]; attributes[attributeMatch[1]] = attributeMatch[2];
} }
content = content.replace(match, `"${attributes.result}"`); if (!attributes.id) {
} continue;
} }
return content; 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 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 // This regular expression matches code blocks marked by triple backticks
const codeBlockRegex = /```[\s\S]*?```/g; const codeBlockRegex = /```[\s\S]*?```/g;