mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
Compare commits
23 commits
045bb3e185
...
e11b6a610d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e11b6a610d | ||
|
|
5f5a8fa7bd | ||
|
|
93feadd93b | ||
|
|
23faf18ae4 | ||
|
|
ab10e84f96 | ||
|
|
606ab164ba | ||
|
|
9ba90414eb | ||
|
|
fda3b287d4 | ||
|
|
00b61ee25c | ||
|
|
7e3d7b8a00 | ||
|
|
098d27c487 | ||
|
|
b78d28e5cc | ||
|
|
6046e52c76 | ||
|
|
0790cc8ef4 | ||
|
|
0422b5e29d | ||
|
|
d10a71c298 | ||
|
|
96c7c948a1 | ||
|
|
fc68071e1d | ||
|
|
140605e660 | ||
|
|
9899293f05 | ||
|
|
e3faec62c5 | ||
|
|
fc05e0a6c5 | ||
|
|
fe6783c166 |
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_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
|
||||||
####################################
|
####################################
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue