From d9b6d78d5ce8f21f5aedc03a65e31288972de439 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 27 Mar 2025 01:38:35 -0700 Subject: [PATCH] feat: external tool server support frontend --- backend/open_webui/utils/middleware.py | 24 +- backend/open_webui/utils/tools.py | 5 +- src/lib/components/AddServerModal.svelte | 212 ++++++++++++++++++ src/lib/components/chat/Chat.svelte | 7 + src/lib/components/chat/MessageInput.svelte | 51 ++++- src/lib/components/chat/Placeholder.svelte | 3 + src/lib/components/chat/Settings/Tools.svelte | 115 ++++++++++ .../chat/Settings/Tools/Connection.svelte | 96 ++++++++ src/lib/components/chat/SettingsModal.svelte | 41 ++++ 9 files changed, 536 insertions(+), 18 deletions(-) create mode 100644 src/lib/components/AddServerModal.svelte create mode 100644 src/lib/components/chat/Settings/Tools.svelte create mode 100644 src/lib/components/chat/Settings/Tools/Connection.svelte diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 2ba892b9f..f6d81214e 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -215,6 +215,7 @@ async def chat_completion_tools_handler( "id": str(uuid4()), "tool": tool, "params": tool_function_params, + "server": tool.get("server", {}), "session_id": metadata.get("session_id", None), }, } @@ -787,10 +788,10 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Server side tools tool_ids = metadata.get("tool_ids", None) # Client side tools - tool_specs = form_data.get("tool_specs", None) + tool_servers = form_data.get("tool_servers", None) log.debug(f"{tool_ids=}") - log.debug(f"{tool_specs=}") + log.debug(f"{tool_servers=}") tools_dict = {} @@ -808,14 +809,16 @@ async def process_chat_payload(request, form_data, user, metadata, model): ) log.info(f"{tools_dict=}") - if tool_specs: - for tool in tool_specs: - callable = tool.pop("callable", None) - tools_dict[tool["name"]] = { - "direct": True, - "callable": callable, - "spec": tool, - } + if tool_servers: + for tool_server in tool_servers: + tool_specs = tool_server.pop("specs", []) + + for tool in tool_specs: + tools_dict[tool["name"]] = { + "spec": tool, + "direct": True, + "server": tool_server, + } if tools_dict: if metadata.get("function_calling") == "native": @@ -1823,6 +1826,7 @@ async def process_chat_response( "id": str(uuid4()), "tool": tool, "params": tool_function_params, + "server": tool.get("server", {}), "session_id": metadata.get( "session_id", None ), diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 53ecf4d0e..bd2a731e6 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -91,10 +91,11 @@ def get_tools( # TODO: This needs to be a pydantic model tool_dict = { - "toolkit_id": tool_id, - "callable": callable, "spec": spec, + "callable": callable, + "toolkit_id": tool_id, "pydantic_model": function_to_pydantic_model(callable), + # Misc info "file_handler": hasattr(module, "file_handler") and module.file_handler, "citation": hasattr(module, "citation") and module.citation, } diff --git a/src/lib/components/AddServerModal.svelte b/src/lib/components/AddServerModal.svelte new file mode 100644 index 000000000..fed9f0477 --- /dev/null +++ b/src/lib/components/AddServerModal.svelte @@ -0,0 +1,212 @@ + + + +
+
+
+ {#if edit} + {$i18n.t('Edit Connection')} + {:else} + {$i18n.t('Add Connection')} + {/if} +
+ +
+ +
+
+
{ + e.preventDefault(); + submitHandler(); + }} + > +
+
+
+
{$i18n.t('URL')}
+ +
+ +
+
+ +
+ + + +
+
+ +
+ {$i18n.t(`WebUI will make requests to "{{URL}}/openapi.json"`, { + URL: url + })} +
+ +
+
+
{$i18n.t('Key')}
+ +
+ +
+
+
+
+ +
+ {#if edit} + + {/if} + + +
+
+
+
+
+
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 2892d436c..fe733d616 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -119,6 +119,9 @@ let imageGenerationEnabled = false; let webSearchEnabled = false; let codeInterpreterEnabled = false; + + let toolServers = []; + let chat = null; let tags = []; @@ -191,6 +194,8 @@ setToolIds(); } + $: toolServers = ($settings?.toolServers ?? []).filter((server) => server?.config?.enable); + const setToolIds = async () => { if (!$tools) { tools.set(await getTools(localStorage.token)); @@ -2033,6 +2038,7 @@ bind:codeInterpreterEnabled bind:webSearchEnabled bind:atSelectedModel + {toolServers} transparentBackground={$settings?.backgroundImageUrl ?? false} {stopResponse} {createMessagePair} @@ -2086,6 +2092,7 @@ bind:webSearchEnabled bind:atSelectedModel transparentBackground={$settings?.backgroundImageUrl ?? false} + {toolServers} {stopResponse} {createMessagePair} on:upload={async (e) => { diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 7db31010b..a1f82ed44 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -68,6 +68,8 @@ export let prompt = ''; export let files = []; + export let toolServers = []; + export let selectedToolIds = []; export let imageGenerationEnabled = false; @@ -1175,14 +1177,14 @@ @@ -1195,13 +1197,13 @@ on:click|preventDefault={() => (imageGenerationEnabled = !imageGenerationEnabled)} type="button" - class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled + class="px-1.5 @lg:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled ? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400' : 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}" > @@ -1214,13 +1216,13 @@ on:click|preventDefault={() => (codeInterpreterEnabled = !codeInterpreterEnabled)} type="button" - class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {codeInterpreterEnabled + class="px-1.5 @lg:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {codeInterpreterEnabled ? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400' : 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}" > @@ -1231,6 +1233,43 @@
+ {#if toolServers.length > 0} + +
+ + + + + + + {toolServers.length} + +
+
+ {/if} + {#if !history?.currentId || history.messages[history.currentId]?.done == true} + +
+ +
+ {#each servers as server, idx} + { + updateHandler(); + }} + onDelete={() => { + servers = servers.filter((_, i) => i !== idx); + updateHandler(); + }} + /> + {/each} +
+ + +
+
+ {$i18n.t('Connect to your own OpenAPI compatible external tool servers.')} +
+ {$i18n.t( + 'CORS must be properly configured by the provider to allow requests from Open WebUI.' + )} +
+
+ + + {:else} +
+
+ +
+
+ {/if} + + +
+ +
+ diff --git a/src/lib/components/chat/Settings/Tools/Connection.svelte b/src/lib/components/chat/Settings/Tools/Connection.svelte new file mode 100644 index 000000000..b61bac878 --- /dev/null +++ b/src/lib/components/chat/Settings/Tools/Connection.svelte @@ -0,0 +1,96 @@ + + + { + showDeleteConfirmDialog = true; + }} + onSubmit={(connection) => { + url = connection.url; + key = connection.key; + config = connection.config; + onSubmit(connection); + }} +/> + + { + onDelete(); + showConfigModal = false; + }} +/> + +
+ + {#if !(config?.enable ?? true)} +
+ {/if} +
+
+ +
+ + +
+
+ +
+ + + +
+
diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 7d32a9718..1e341f380 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -17,6 +17,7 @@ import Personalization from './Settings/Personalization.svelte'; import Search from '../icons/Search.svelte'; import Connections from './Settings/Connections.svelte'; + import Tools from './Settings/Tools.svelte'; const i18n = getContext('i18n'); @@ -127,6 +128,11 @@ title: 'Connections', keywords: [] }, + { + id: 'tools', + title: 'Tools', + keywords: [] + }, { id: 'personalization', title: 'Personalization', @@ -481,6 +487,34 @@
{$i18n.t('Connections')}
{/if} + {:else if tabId === 'tools'} + {#if $user.role === 'admin' || ($user.role === 'user' && $config?.features?.enable_direct_tools)} + + {/if} {:else if tabId === 'personalization'}