diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py
index e8ce3aa811..2020a29633 100644
--- a/backend/open_webui/models/functions.py
+++ b/backend/open_webui/models/functions.py
@@ -3,7 +3,7 @@ import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
-from open_webui.models.users import Users
+from open_webui.models.users import Users, UserModel
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index
@@ -76,6 +76,10 @@ class FunctionWithValvesModel(BaseModel):
####################
+class FunctionUserResponse(FunctionModel):
+ user: Optional[UserModel] = None
+
+
class FunctionResponse(BaseModel):
id: str
user_id: str
@@ -203,6 +207,28 @@ class FunctionsTable:
FunctionModel.model_validate(function) for function in functions
]
+ def get_function_list(self) -> list[FunctionUserResponse]:
+ with get_db() as db:
+ functions = db.query(Function).order_by(Function.updated_at.desc()).all()
+ user_ids = list(set(func.user_id for func in functions))
+
+ users = Users.get_users_by_user_ids(user_ids) if user_ids else []
+ users_dict = {user.id: user for user in users}
+
+ return [
+ FunctionUserResponse.model_validate(
+ {
+ **FunctionModel.model_validate(func).model_dump(),
+ "user": (
+ users_dict.get(func.user_id).model_dump()
+ if func.user_id in users_dict
+ else None
+ ),
+ }
+ )
+ for func in functions
+ ]
+
def get_functions_by_type(
self, type: str, active_only=False
) -> list[FunctionModel]:
diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py
index c36e656d5f..c8f131553c 100644
--- a/backend/open_webui/routers/functions.py
+++ b/backend/open_webui/routers/functions.py
@@ -10,6 +10,7 @@ from open_webui.models.functions import (
FunctionForm,
FunctionModel,
FunctionResponse,
+ FunctionUserResponse,
FunctionWithValvesModel,
Functions,
)
@@ -42,6 +43,11 @@ async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions()
+@router.get("/list", response_model=list[FunctionUserResponse])
+async def get_function_list(user=Depends(get_admin_user)):
+ return Functions.get_function_list()
+
+
############################
# ExportFunctions
############################
diff --git a/src/lib/apis/functions/index.ts b/src/lib/apis/functions/index.ts
index 60e88118b8..47346b4a20 100644
--- a/src/lib/apis/functions/index.ts
+++ b/src/lib/apis/functions/index.ts
@@ -62,6 +62,37 @@ export const getFunctions = async (token: string = '') => {
return res;
};
+export const getFunctionList = async (token: string = '') => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/functions/list`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.error(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
export const loadFunctionByUrl = async (token: string = '', url: string) => {
let error = null;
diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte
index 0d1e7edadc..4bbcebd15f 100644
--- a/src/lib/components/admin/Functions.svelte
+++ b/src/lib/components/admin/Functions.svelte
@@ -3,7 +3,7 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
- import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores';
+ import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
import { onMount, getContext, tick } from 'svelte';
import { goto } from '$app/navigation';
@@ -12,6 +12,7 @@
deleteFunctionById,
exportFunctions,
getFunctionById,
+ getFunctionList,
getFunctions,
loadFunctionByUrl,
toggleFunctionById,
@@ -36,6 +37,10 @@
import XMark from '../icons/XMark.svelte';
import AddFunctionMenu from './Functions/AddFunctionMenu.svelte';
import ImportModal from '../ImportModal.svelte';
+ import ViewSelector from '../workspace/common/ViewSelector.svelte';
+ import TagSelector from '../workspace/common/TagSelector.svelte';
+ import { capitalizeFirstLetter } from '$lib/utils';
+ import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n');
@@ -44,12 +49,16 @@
let functionsImportInputElement: HTMLInputElement;
let importFiles;
+ let tagsContainerElement: HTMLDivElement;
+ let viewOption = '';
+
+ let query = '';
+ let selectedTag = '';
+ let selectedType = '';
+
let showImportModal = false;
let showConfirm = false;
- let query = '';
-
- let selectedType = 'all';
let showManifestModal = false;
let showValvesModal = false;
@@ -57,17 +66,33 @@
let showDeleteConfirm = false;
+ let loaded = false;
+ let functions = null;
let filteredItems = [];
- $: filteredItems = $functions
- .filter(
- (f) =>
- (selectedType !== 'all' ? f.type === selectedType : true) &&
- (query === '' ||
- f.name.toLowerCase().includes(query.toLowerCase()) ||
- f.id.toLowerCase().includes(query.toLowerCase()))
- )
- .sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
+ $: if (
+ functions &&
+ query !== undefined &&
+ selectedType !== undefined &&
+ viewOption !== undefined
+ ) {
+ setFilteredItems();
+ }
+
+ const setFilteredItems = () => {
+ filteredItems = functions
+ .filter(
+ (f) =>
+ (selectedType !== '' ? f.type === selectedType : true) &&
+ (query === '' ||
+ f.name.toLowerCase().includes(query.toLowerCase()) ||
+ f.id.toLowerCase().includes(query.toLowerCase())) &&
+ (viewOption === '' ||
+ (viewOption === 'created' && f.user_id === $user?.id) ||
+ (viewOption === 'shared' && f.user_id !== $user?.id))
+ )
+ .sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
+ };
const shareHandler = async (func) => {
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(`${error}`);
@@ -134,7 +159,7 @@
if (res) {
toast.success($i18n.t('Function deleted successfully'));
- functions.set(await getFunctions(localStorage.token));
+ _functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
@@ -162,7 +187,7 @@
: toast.success($i18n.t('Function is now globally disabled'));
}
- functions.set(await getFunctions(localStorage.token));
+ _functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
@@ -174,7 +199,16 @@
}
};
- onMount(() => {
+ onMount(async () => {
+ viewOption = localStorage?.workspaceViewOption || '';
+ functions = await getFunctionList(localStorage.token).catch((error) => {
+ toast.error(`${error}`);
+ return [];
+ });
+
+ await tick();
+ loaded = true;
+
const onKeyDown = (event) => {
if (event.key === 'Shift') {
shiftKey = true;
@@ -222,424 +256,388 @@
}}
/>
-
-
-
- {$i18n.t('Functions')}
-
-
{filteredItems.length}
-
-
+{#if loaded}
+
+
+
+
{
+ console.log(importFiles);
+ showConfirm = true;
+ }}
+ />
-
-
-
-
+
+
+
+ {$i18n.t('Functions')}
+
+
+
+ {filteredItems.length}
+
+
+
+
+ {#if $user?.role === 'admin'}
+
{
+ functionsImportInputElement.click();
+ }}
+ >
+
+ {$i18n.t('Import')}
+
+
+
+ {#if functions.length}
+
{
+ const _functions = await exportFunctions(localStorage.token).catch((error) => {
+ toast.error(`${error}`);
+ return null;
+ });
+
+ if (_functions) {
+ let blob = new Blob([JSON.stringify(_functions)], {
+ type: 'application/json'
+ });
+ saveAs(blob, `functions-export-${Date.now()}.json`);
+ }
+ }}
+ >
+
+ {$i18n.t('Export')}
+
+
+ {/if}
+ {/if}
+
{
+ goto('/admin/functions/create');
+ }}
+ importFromLinkHandler={() => {
+ showImportModal = true;
+ }}
+ >
+
+
+
+
{$i18n.t('New Function')}
+
+
+
+
-
+
- {#if query}
-
-
{
- query = '';
+
+
+
+
+
+
+
+
+ {#if query}
+
+ {
+ query = '';
+ }}
+ >
+
+
+
+ {/if}
+
+
+
+
+
+ {#if (filteredItems ?? []).length !== 0}
+
+ {#each filteredItems as func (func.id)}
+
+ {/each}
+
+ {:else}
+
+
+
😕
+
{$i18n.t('No functions found')}
+
+ {$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
+
+
{/if}
-
-
{
- goto('/admin/functions/create');
- }}
- importFromLinkHandler={() => {
- showImportModal = true;
- }}
- >
-
-
-
-
-
-
-
- {
- selectedType = 'all';
- }}>{$i18n.t('All')}
-
- {
- selectedType = 'pipe';
- }}>{$i18n.t('Pipe')}
-
- {
- selectedType = 'filter';
- }}>{$i18n.t('Filter')}
-
- {
- selectedType = 'action';
- }}>{$i18n.t('Action')}
-
-
-
-
-
- {#each filteredItems as func (func.id)}
-
-
-
-
-
-
- {func.type}
-
-
- {#if func?.meta?.manifest?.version}
-
- v{func?.meta?.manifest?.version ?? ''}
-
- {/if}
-
-
- {func.name}
-
-
-
-
-
{func.id}
-
-
- {func.meta.description}
-
-
-
-
-
-
- {#if shiftKey}
-
- {
- deleteHandler(func);
- }}
- >
-
-
-
- {:else}
- {#if func?.meta?.manifest?.funding_url ?? false}
-
- {
- selectedFunction = func;
- showManifestModal = true;
- }}
- >
-
-
-
- {/if}
-
-
- {
- selectedFunction = func;
- showValvesModal = true;
- }}
- >
-
-
-
-
-
-
-
-
{
- goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
- }}
- shareHandler={() => {
- shareHandler(func);
- }}
- cloneHandler={() => {
- cloneHandler(func);
- }}
- exportHandler={() => {
- exportHandler(func);
- }}
- deleteHandler={async () => {
- selectedFunction = func;
- showDeleteConfirm = true;
- }}
- toggleGlobalHandler={() => {
- if (['filter', 'action'].includes(func.type)) {
- toggleGlobalHandler(func);
- }
- }}
- onClose={() => {}}
- >
-
-
-
-
- {/if}
-
-
-
- {
- toggleFunctionById(localStorage.token, func.id);
- models.set(
- await getModels(
- localStorage.token,
- $config?.features?.enable_direct_connections &&
- ($settings?.directConnections ?? null),
- false,
- true
- )
- );
- }}
- />
-
-
-
-
- {/each}
-
-
-
-
-
-
{
- console.log(importFiles);
- showConfirm = true;
- }}
- />
+ {#if $config?.features.enable_community_sharing}
+
+
+ {$i18n.t('Made by Open WebUI Community')}
+
-
{
- functionsImportInputElement.click();
- }}
- >
- {$i18n.t('Import Functions')}
-
-
-
-
-
+
+
{$i18n.t('Discover a function')}
+
+ {$i18n.t('Discover, download, and explore custom functions')}
+
+
+
+
+
-
-
- {#if $functions.length}
-
{
- const _functions = await exportFunctions(localStorage.token).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
-
- if (_functions) {
- let blob = new Blob([JSON.stringify(_functions)], {
- type: 'application/json'
- });
- saveAs(blob, `functions-export-${Date.now()}.json`);
- }
- }}
- >
-
- {$i18n.t('Export Functions')} ({$functions.length})
-
-
-
-
{/if}
-
-{#if $config?.features.enable_community_sharing}
-
-
- {$i18n.t('Made by Open WebUI Community')}
+
{
+ deleteHandler(selectedFunction);
+ }}
+ >
+
+ {$i18n.t('This will delete')} {selectedFunction.name} .
+
-
-
-
{$i18n.t('Discover a function')}
-
- {$i18n.t('Discover, download, and explore custom functions')}
-
-
-
-
-
-
-{/if}
-
-
{
- deleteHandler(selectedFunction);
- }}
->
-
- {$i18n.t('This will delete')} {selectedFunction.name} .
-
-
-
-
-
{
- await tick();
- models.set(
- await getModels(
- localStorage.token,
- $config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
- false,
- true
- )
- );
- }}
-/>
-
- {
- const reader = new FileReader();
- reader.onload = async (event) => {
- const _functions = JSON.parse(event.target.result);
- console.log(_functions);
-
- for (let func of _functions) {
- if ('function' in func) {
- // Required for Community JSON import
- func = func.function;
- }
-
- const res = await createNewFunction(localStorage.token, func).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
- }
-
- toast.success($i18n.t('Functions imported successfully'));
- functions.set(await getFunctions(localStorage.token));
+
+ {
+ await tick();
models.set(
await getModels(
localStorage.token,
@@ -648,25 +646,63 @@
true
)
);
- };
+ }}
+ />
- reader.readAsText(importFiles[0]);
- }}
->
-
-
-
{$i18n.t('Please carefully review the following warnings:')}
+
{
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ const _functions = JSON.parse(event.target.result);
+ console.log(_functions);
-
- {$i18n.t('Functions allow arbitrary code execution.')}
- {$i18n.t('Do not install functions from sources you do not fully trust.')}
-
-
-
-
- {$i18n.t(
- 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
- )}
+ for (let func of _functions) {
+ if ('function' in func) {
+ // Required for Community JSON import
+ func = func.function;
+ }
+
+ const res = await createNewFunction(localStorage.token, func).catch((error) => {
+ toast.error(`${error}`);
+ return null;
+ });
+ }
+
+ toast.success($i18n.t('Functions imported successfully'));
+ functions.set(await getFunctions(localStorage.token));
+ models.set(
+ await getModels(
+ localStorage.token,
+ $config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
+ false,
+ true
+ )
+ );
+ };
+
+ reader.readAsText(importFiles[0]);
+ }}
+ >
+
+
+
{$i18n.t('Please carefully review the following warnings:')}
+
+
+ {$i18n.t('Functions allow arbitrary code execution.')}
+ {$i18n.t('Do not install functions from sources you do not fully trust.')}
+
+
+
+
+ {$i18n.t(
+ 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
+ )}
+
+
+{:else}
+
+
-
+{/if}
diff --git a/src/lib/components/admin/Functions/AddFunctionMenu.svelte b/src/lib/components/admin/Functions/AddFunctionMenu.svelte
index 2c3412fdfc..0272bac298 100644
--- a/src/lib/components/admin/Functions/AddFunctionMenu.svelte
+++ b/src/lib/components/admin/Functions/AddFunctionMenu.svelte
@@ -41,27 +41,27 @@
{
createHandler();
show = false;
}}
>
{$i18n.t('New Function')}
{
importFromLinkHandler();
show = false;