mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
User placeholders added for dynamic mcp header configuration
This commit is contained in:
parent
b32f7815b8
commit
aa4e9dc74e
7 changed files with 364 additions and 4 deletions
|
|
@ -147,6 +147,7 @@ class ToolServerConnection(BaseModel):
|
|||
headers: Optional[dict | str] = None
|
||||
key: Optional[str]
|
||||
config: Optional[dict]
|
||||
placeholders: Optional[list[str]] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ from open_webui.utils.misc import (
|
|||
convert_logit_bias_input_to_json,
|
||||
get_content_from_message,
|
||||
)
|
||||
from open_webui.utils.tools import get_tools, get_updated_tool_function
|
||||
from open_webui.utils.tools import (
|
||||
get_tools,
|
||||
get_updated_tool_function,
|
||||
replace_placeholders_in_headers
|
||||
)
|
||||
from open_webui.utils.plugin import load_function_module_by_id
|
||||
from open_webui.utils.filter import (
|
||||
get_sorted_filter_ids,
|
||||
|
|
@ -1142,10 +1146,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
except Exception as e:
|
||||
log.error(f"Error getting OAuth token: {e}")
|
||||
|
||||
__user__ = user.model_dump() if isinstance(user, UserModel) else {}
|
||||
if isinstance(user, UserModel) and user.settings:
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
if "tool_server_placeholders" in user_settings:
|
||||
__user__["tool_server_placeholders"] = user_settings["tool_server_placeholders"]
|
||||
|
||||
extra_params = {
|
||||
"__event_emitter__": event_emitter,
|
||||
"__event_call__": event_caller,
|
||||
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
||||
"__user__": __user__,
|
||||
"__metadata__": metadata,
|
||||
"__oauth_token__": oauth_token,
|
||||
"__request__": request,
|
||||
|
|
@ -1404,6 +1414,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
|
||||
connection_headers = mcp_server_connection.get("headers", None)
|
||||
if connection_headers and isinstance(connection_headers, dict):
|
||||
user_placeholders = (
|
||||
extra_params.get("__user__", {})
|
||||
.get("tool_server_placeholders", {})
|
||||
.get(server_id, {})
|
||||
)
|
||||
|
||||
connection_headers = replace_placeholders_in_headers(
|
||||
connection_headers, user_placeholders
|
||||
)
|
||||
|
||||
for key, value in connection_headers.items():
|
||||
headers[key] = value
|
||||
|
||||
|
|
@ -1981,10 +2001,16 @@ async def process_chat_response(
|
|||
except Exception as e:
|
||||
log.error(f"Error getting OAuth token: {e}")
|
||||
|
||||
__user__ = user.model_dump() if isinstance(user, UserModel) else {}
|
||||
if isinstance(user, UserModel) and user.settings:
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
if "tool_server_placeholders" in user_settings:
|
||||
__user__["tool_server_placeholders"] = user_settings["tool_server_placeholders"]
|
||||
|
||||
extra_params = {
|
||||
"__event_emitter__": event_emitter,
|
||||
"__event_call__": event_caller,
|
||||
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
||||
"__user__": __user__,
|
||||
"__metadata__": metadata,
|
||||
"__oauth_token__": oauth_token,
|
||||
"__request__": request,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,28 @@ log = logging.getLogger(__name__)
|
|||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
|
||||
def replace_placeholders_in_headers(
|
||||
headers: dict, placeholders: dict[str, str]
|
||||
) -> dict:
|
||||
if not headers or not placeholders:
|
||||
return headers
|
||||
|
||||
replaced_headers = {}
|
||||
for key, value in headers.items():
|
||||
if isinstance(value, str):
|
||||
replaced_value = value
|
||||
for placeholder_name, placeholder_value in placeholders.items():
|
||||
placeholder_pattern = f"{{{{{placeholder_name}}}}}"
|
||||
replaced_value = replaced_value.replace(
|
||||
placeholder_pattern, placeholder_value
|
||||
)
|
||||
replaced_headers[key] = replaced_value
|
||||
else:
|
||||
replaced_headers[key] = value
|
||||
|
||||
return replaced_headers
|
||||
|
||||
|
||||
def get_async_tool_function_and_apply_extra_params(
|
||||
function: Callable, extra_params: dict
|
||||
) -> Callable[..., Awaitable]:
|
||||
|
|
@ -181,6 +203,17 @@ async def get_tools(
|
|||
|
||||
connection_headers = tool_server_connection.get("headers", None)
|
||||
if connection_headers and isinstance(connection_headers, dict):
|
||||
user_placeholders = (
|
||||
extra_params.get("__user__", {})
|
||||
.get("tool_server_placeholders", {})
|
||||
.get(server_id, {})
|
||||
)
|
||||
|
||||
# Replace placeholders in headers
|
||||
connection_headers = replace_placeholders_in_headers(
|
||||
connection_headers, user_placeholders
|
||||
)
|
||||
|
||||
for key, value in connection_headers.items():
|
||||
headers[key] = value
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
let auth_type = 'bearer';
|
||||
let key = '';
|
||||
let headers = '';
|
||||
let placeholders: string[] = [];
|
||||
|
||||
let accessControl = {};
|
||||
|
||||
|
|
@ -196,6 +197,7 @@
|
|||
if (data.auth_type) auth_type = data.auth_type;
|
||||
if (data.headers) headers = JSON.stringify(data.headers, null, 2);
|
||||
if (data.key) key = data.key;
|
||||
if (data.placeholders) placeholders = data.placeholders;
|
||||
|
||||
if (data.info) {
|
||||
id = data.info.id ?? '';
|
||||
|
|
@ -230,6 +232,7 @@
|
|||
auth_type,
|
||||
headers: headers ? JSON.parse(headers) : undefined,
|
||||
key,
|
||||
placeholders: placeholders.length > 0 ? placeholders : undefined,
|
||||
|
||||
info: {
|
||||
id: id,
|
||||
|
|
@ -300,6 +303,7 @@
|
|||
headers: headers ? JSON.parse(headers) : undefined,
|
||||
|
||||
key,
|
||||
placeholders: placeholders.length > 0 ? placeholders : undefined,
|
||||
config: {
|
||||
enable: enable,
|
||||
|
||||
|
|
@ -328,6 +332,7 @@
|
|||
|
||||
key = '';
|
||||
auth_type = 'bearer';
|
||||
placeholders = [];
|
||||
|
||||
id = '';
|
||||
name = '';
|
||||
|
|
@ -351,6 +356,7 @@
|
|||
headers = connection?.headers ? JSON.stringify(connection.headers, null, 2) : '';
|
||||
|
||||
key = connection?.key ?? '';
|
||||
placeholders = connection?.placeholders ?? [];
|
||||
|
||||
id = connection.info?.id ?? '';
|
||||
name = connection.info?.name ?? '';
|
||||
|
|
@ -721,6 +727,66 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<label
|
||||
for="placeholders-input"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t('Placeholders')}
|
||||
<span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5">
|
||||
{$i18n.t('Optional')}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Tooltip content={$i18n.t('Add Placeholder')}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
placeholders = [...placeholders, ''];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 mb-2">
|
||||
{$i18n.t(
|
||||
'Define placeholder names that users will need to fill in. Use them in headers as {{PLACEHOLDER_NAME}}'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{#if placeholders.length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each placeholders as placeholder, idx}
|
||||
<div class="flex gap-2 items-center">
|
||||
<input
|
||||
class={`flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={placeholders[idx]}
|
||||
placeholder={$i18n.t('e.g., API_KEY or USER_TOKEN')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
||||
on:click={() => {
|
||||
placeholders = placeholders.filter((_, i) => i !== idx);
|
||||
}}
|
||||
type="button"
|
||||
aria-label={$i18n.t('Remove')}
|
||||
>
|
||||
<Minus />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores';
|
||||
|
||||
import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs';
|
||||
import { getOAuthClientAuthorizationUrl, getToolServerConnections } from '$lib/apis/configs';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
import Knobs from '$lib/components/icons/Knobs.svelte';
|
||||
|
|
@ -21,6 +21,8 @@
|
|||
import Terminal from '$lib/components/icons/Terminal.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
|
||||
import PlaceholderConfigModal from './PlaceholderConfigModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -48,6 +50,10 @@
|
|||
let tab = '';
|
||||
|
||||
let tools = null;
|
||||
let toolServerPlaceholders = {};
|
||||
|
||||
let showPlaceholderModal = false;
|
||||
let selectedServerForPlaceholder = null;
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
|
|
@ -88,6 +94,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getToolServerConnections(localStorage.token);
|
||||
const adminServers = res.TOOL_SERVER_CONNECTIONS || [];
|
||||
|
||||
toolServerPlaceholders = {};
|
||||
for (const server of adminServers) {
|
||||
if (server.placeholders && server.placeholders.length > 0) {
|
||||
const serverId = server.info?.id || server.url;
|
||||
toolServerPlaceholders[serverId] = {
|
||||
name: server.info?.name || server.url,
|
||||
placeholders: server.placeholders,
|
||||
serverId: serverId
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch admin tool servers:', err);
|
||||
}
|
||||
|
||||
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id));
|
||||
};
|
||||
</script>
|
||||
|
|
@ -392,6 +417,28 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))}
|
||||
{@const serverData = toolServerPlaceholders[
|
||||
Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))
|
||||
]}
|
||||
<div class=" shrink-0">
|
||||
<Tooltip content={$i18n.t('Configure Placeholders')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
selectedServerForPlaceholder = serverData;
|
||||
showPlaceholderModal = true;
|
||||
}}
|
||||
>
|
||||
<PencilSolid />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" shrink-0">
|
||||
<Switch state={tools[toolId].enabled} />
|
||||
</div>
|
||||
|
|
@ -402,3 +449,10 @@
|
|||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
<PlaceholderConfigModal
|
||||
bind:show={showPlaceholderModal}
|
||||
serverName={selectedServerForPlaceholder?.name || ''}
|
||||
serverId={selectedServerForPlaceholder?.serverId || ''}
|
||||
placeholders={selectedServerForPlaceholder?.placeholders || []}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let serverName: string = '';
|
||||
export let serverId: string = '';
|
||||
export let placeholders: string[] = [];
|
||||
|
||||
let placeholderValues: { [key: string]: string } = {};
|
||||
|
||||
$: if (show && placeholders) {
|
||||
// Initialize values from settings
|
||||
const savedValues = $settings?.tool_server_placeholders?.[serverId] || {};
|
||||
placeholderValues = placeholders.reduce((acc, placeholder) => {
|
||||
acc[placeholder] = savedValues[placeholder] || '';
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
}
|
||||
|
||||
const saveHandler = async () => {
|
||||
try {
|
||||
const currentPlaceholders = $settings?.tool_server_placeholders || {};
|
||||
const updatedPlaceholders = {
|
||||
...currentPlaceholders,
|
||||
[serverId]: placeholderValues
|
||||
};
|
||||
|
||||
await updateUserSettings(localStorage.token, {
|
||||
tool_server_placeholders: updatedPlaceholders
|
||||
});
|
||||
|
||||
// Update local settings
|
||||
settings.set({
|
||||
...$settings,
|
||||
tool_server_placeholders: updatedPlaceholders
|
||||
});
|
||||
|
||||
toast.success($i18n.t('Placeholder values saved'));
|
||||
show = false;
|
||||
} catch (error) {
|
||||
toast.error($i18n.t('Failed to save placeholder values'));
|
||||
console.error('Error saving placeholder values:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-2">
|
||||
<h1 class="text-lg font-medium self-center font-primary">
|
||||
{$i18n.t('Configure Placeholders')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col px-5 pb-4">
|
||||
<div class="mb-3">
|
||||
<div class="text-sm font-medium mb-1">{serverName}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t(
|
||||
'These values will be used to replace placeholders in the server headers. Your values are private and only visible to you.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
on:submit|preventDefault={saveHandler}
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
{#each placeholders as placeholder}
|
||||
<div class="flex flex-col gap-1">
|
||||
<label
|
||||
for={`placeholder-${placeholder}`}
|
||||
class="text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{placeholder}
|
||||
</label>
|
||||
<SensitiveInput
|
||||
id={`placeholder-${placeholder}`}
|
||||
bind:value={placeholderValues[placeholder]}
|
||||
placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })}
|
||||
required={false}
|
||||
/>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Used in headers as')} <code class="text-xs">{'{{' + placeholder + '}}'}</code>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="flex justify-end pt-3 gap-2">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let serverName: string = '';
|
||||
export let serverId: string = '';
|
||||
export let placeholders: string[] = [];
|
||||
export let values: { [key: string]: string } = {};
|
||||
export let onChange: (values: { [key: string]: string }) => void = () => {};
|
||||
|
||||
// Initialize values if not set
|
||||
$: if (placeholders && !Object.keys(values).length) {
|
||||
values = placeholders.reduce((acc, placeholder) => {
|
||||
acc[placeholder] = '';
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
}
|
||||
|
||||
const handleValueChange = (placeholder: string, value: string) => {
|
||||
values[placeholder] = value;
|
||||
onChange(values);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if placeholders && placeholders.length > 0}
|
||||
<div class="flex flex-col gap-2 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-medium text-sm">{serverName}</div>
|
||||
{#if serverId}
|
||||
<div class="text-xs text-gray-500">{serverId}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each placeholders as placeholder}
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for={`${serverId}-${placeholder}`} class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{placeholder}
|
||||
</label>
|
||||
<SensitiveInput
|
||||
id={`${serverId}-${placeholder}`}
|
||||
value={values[placeholder] || ''}
|
||||
on:input={(e) => handleValueChange(placeholder, e.target.value)}
|
||||
placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })}
|
||||
required={false}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'These values will be used to replace placeholders in the server headers (e.g., {{{{PLACEHOLDER_NAME}}}})'
|
||||
)}
|
||||
>
|
||||
<span class="underline cursor-help">{$i18n.t('What are placeholders?')}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Loading…
Reference in a new issue