mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
Merge 5f5a8fa7bd into 4c4b9d19a1
This commit is contained in:
commit
045bb3e185
4 changed files with 161 additions and 53 deletions
|
|
@ -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
|
||||
####################################
|
||||
|
|
|
|||
|
|
@ -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}<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}"
|
||||
|
|
@ -2088,7 +2098,7 @@ async def process_chat_response(
|
|||
"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}"
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue