Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
27f8afeb
Commit
27f8afeb
authored
Jun 20, 2024
by
Timothy J. Baek
Browse files
feat: function db migration
parent
f68aba68
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
334 additions
and
54 deletions
+334
-54
backend/apps/webui/internal/migrations/015_add_functions.py
backend/apps/webui/internal/migrations/015_add_functions.py
+61
-0
backend/apps/webui/main.py
backend/apps/webui/main.py
+7
-3
src/lib/apis/functions/index.ts
src/lib/apis/functions/index.ts
+193
-0
src/lib/components/workspace/Functions.svelte
src/lib/components/workspace/Functions.svelte
+73
-51
No files found.
backend/apps/webui/internal/migrations/015_add_functions.py
0 → 100644
View file @
27f8afeb
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from
contextlib
import
suppress
import
peewee
as
pw
from
peewee_migrate
import
Migrator
with
suppress
(
ImportError
):
import
playhouse.postgres_ext
as
pw_pext
def
migrate
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your migrations here."""
@
migrator
.
create_model
class
Function
(
pw
.
Model
):
id
=
pw
.
TextField
(
unique
=
True
)
user_id
=
pw
.
TextField
()
name
=
pw
.
TextField
()
type
=
pw
.
TextField
()
content
=
pw
.
TextField
()
meta
=
pw
.
TextField
()
created_at
=
pw
.
BigIntegerField
(
null
=
False
)
updated_at
=
pw
.
BigIntegerField
(
null
=
False
)
class
Meta
:
table_name
=
"function"
def
rollback
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your rollback migrations here."""
migrator
.
remove_model
(
"function"
)
backend/apps/webui/main.py
View file @
27f8afeb
...
@@ -13,6 +13,7 @@ from apps.webui.routers import (
...
@@ -13,6 +13,7 @@ from apps.webui.routers import (
memories
,
memories
,
utils
,
utils
,
files
,
files
,
functions
,
)
)
from
config
import
(
from
config
import
(
WEBUI_BUILD_HASH
,
WEBUI_BUILD_HASH
,
...
@@ -70,19 +71,22 @@ app.add_middleware(
...
@@ -70,19 +71,22 @@ app.add_middleware(
allow_headers
=
[
"*"
],
allow_headers
=
[
"*"
],
)
)
app
.
include_router
(
configs
.
router
,
prefix
=
"/configs"
,
tags
=
[
"configs"
])
app
.
include_router
(
auths
.
router
,
prefix
=
"/auths"
,
tags
=
[
"auths"
])
app
.
include_router
(
auths
.
router
,
prefix
=
"/auths"
,
tags
=
[
"auths"
])
app
.
include_router
(
users
.
router
,
prefix
=
"/users"
,
tags
=
[
"users"
])
app
.
include_router
(
users
.
router
,
prefix
=
"/users"
,
tags
=
[
"users"
])
app
.
include_router
(
chats
.
router
,
prefix
=
"/chats"
,
tags
=
[
"chats"
])
app
.
include_router
(
chats
.
router
,
prefix
=
"/chats"
,
tags
=
[
"chats"
])
app
.
include_router
(
documents
.
router
,
prefix
=
"/documents"
,
tags
=
[
"documents"
])
app
.
include_router
(
documents
.
router
,
prefix
=
"/documents"
,
tags
=
[
"documents"
])
app
.
include_router
(
tools
.
router
,
prefix
=
"/tools"
,
tags
=
[
"tools"
])
app
.
include_router
(
models
.
router
,
prefix
=
"/models"
,
tags
=
[
"models"
])
app
.
include_router
(
models
.
router
,
prefix
=
"/models"
,
tags
=
[
"models"
])
app
.
include_router
(
prompts
.
router
,
prefix
=
"/prompts"
,
tags
=
[
"prompts"
])
app
.
include_router
(
prompts
.
router
,
prefix
=
"/prompts"
,
tags
=
[
"prompts"
])
app
.
include_router
(
memories
.
router
,
prefix
=
"/memories"
,
tags
=
[
"memories"
])
app
.
include_router
(
memories
.
router
,
prefix
=
"/memories"
,
tags
=
[
"memories"
])
app
.
include_router
(
files
.
router
,
prefix
=
"/files"
,
tags
=
[
"files"
])
app
.
include_router
(
tools
.
router
,
prefix
=
"/tools"
,
tags
=
[
"tools"
])
app
.
include_router
(
functions
.
router
,
prefix
=
"/functions"
,
tags
=
[
"functions"
])
app
.
include_router
(
configs
.
router
,
prefix
=
"/configs"
,
tags
=
[
"configs"
])
app
.
include_router
(
utils
.
router
,
prefix
=
"/utils"
,
tags
=
[
"utils"
])
app
.
include_router
(
utils
.
router
,
prefix
=
"/utils"
,
tags
=
[
"utils"
])
app
.
include_router
(
files
.
router
,
prefix
=
"/files"
,
tags
=
[
"files"
])
@
app
.
get
(
"/"
)
@
app
.
get
(
"/"
)
...
...
src/lib/apis/functions/index.ts
0 → 100644
View file @
27f8afeb
import
{
WEBUI_API_BASE_URL
}
from
'
$lib/constants
'
;
export
const
createNewFunction
=
async
(
token
:
string
,
func
:
object
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/create`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
...
func
})
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
getFunctions
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/`
,
{
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
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
exportFunctions
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/export`
,
{
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
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
getFunctionById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/id/
${
id
}
`
,
{
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
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
updateFunctionById
=
async
(
token
:
string
,
id
:
string
,
func
:
object
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/id/
${
id
}
/update`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
...
func
})
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
then
((
json
)
=>
{
return
json
;
})
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
deleteFunctionById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/id/
${
id
}
/delete`
,
{
method
:
'
DELETE
'
,
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
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
src/lib/components/workspace/Functions.svelte
View file @
27f8afeb
...
@@ -3,29 +3,39 @@
...
@@ -3,29 +3,39 @@
import fileSaver from 'file-saver';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
const { saveAs } = fileSaver;
import { WEBUI_NAME } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, prompts, tools } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { goto } from '$app/navigation';
import { goto } from '$app/navigation';
import {
import {
createNewTool,
createNewFunction,
deleteToolById,
deleteFunctionById,
exportTools,
exportFunctions,
getToolById,
getFunctionById,
getTools
getFunctions
} from '$lib/apis/tools';
} from '$lib/apis/functions';
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
import Tooltip from '../common/Tooltip.svelte';
import Tooltip from '../common/Tooltip.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
let
tool
sImportInputElement: HTMLInputElement;
let
function
sImportInputElement: HTMLInputElement;
let importFiles;
let importFiles;
let showConfirm = false;
let showConfirm = false;
let query = '';
let query = '';
let functions = [];
onMount(async () => {
functions = await getFunctions(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
});
</script>
</script>
<svelte:head>
<svelte:head>
...
@@ -82,30 +92,30 @@
...
@@ -82,30 +92,30 @@
<hr class=" dark:border-gray-850 my-2.5" />
<hr class=" dark:border-gray-850 my-2.5" />
<div class="my-3 mb-5">
<div class="my-3 mb-5">
{#each
$tool
s.filter((
t
) => query === '' ||
t
.name
{#each
function
s.filter((
f
) => query === '' ||
f
.name
.toLowerCase()
.toLowerCase()
.includes(query.toLowerCase()) ||
t
.id.toLowerCase().includes(query.toLowerCase())) as
tool
}
.includes(query.toLowerCase()) ||
f
.id.toLowerCase().includes(query.toLowerCase())) as
func
}
<button
<button
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
type="button"
type="button"
on:click={() => {
on:click={() => {
goto(`/workspace/
tool
s/edit?id=${encodeURIComponent(
tool
.id)}`);
goto(`/workspace/
function
s/edit?id=${encodeURIComponent(
func
.id)}`);
}}
}}
>
>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a
<a
href={`/workspace/
tool
s/edit?id=${encodeURIComponent(
tool
.id)}`}
href={`/workspace/
function
s/edit?id=${encodeURIComponent(
func
.id)}`}
class="flex items-center text-left"
class="flex items-center text-left"
>
>
<div class=" flex-1 self-center pl-5">
<div class=" flex-1 self-center pl-5">
<div class=" font-semibold flex items-center gap-1.5">
<div class=" font-semibold flex items-center gap-1.5">
<div>
<div>
{
tool
.name}
{
func
.name}
</div>
</div>
<div class=" text-gray-500 text-xs font-medium">{
tool
.id}</div>
<div class=" text-gray-500 text-xs font-medium">{
func
.id}</div>
</div>
</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{
tool
.meta.description}
{
func
.meta.description}
</div>
</div>
</div>
</div>
</a>
</a>
...
@@ -115,7 +125,7 @@
...
@@ -115,7 +125,7 @@
<a
<a
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
type="button"
href={`/workspace/
tool
s/edit?id=${encodeURIComponent(
tool
.id)}`}
href={`/workspace/
function
s/edit?id=${encodeURIComponent(
func
.id)}`}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
...
@@ -141,18 +151,20 @@
...
@@ -141,18 +151,20 @@
on:click={async (e) => {
on:click={async (e) => {
e.stopPropagation();
e.stopPropagation();
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
const _function = await getFunctionById(localStorage.token, func.id).catch(
toast.error(error);
(error) => {
return null;
toast.error(error);
});
return null;
}
if (_tool) {
);
sessionStorage.tool = JSON.stringify({
..._tool,
if (_function) {
id: `${_tool.id}_clone`,
sessionStorage.function = JSON.stringify({
name: `${_tool.name} (Clone)`
..._function,
id: `${_function.id}_clone`,
name: `${_function.name} (Clone)`
});
});
goto('/workspace/
tool
s/create');
goto('/workspace/
function
s/create');
}
}
}}
}}
>
>
...
@@ -180,16 +192,18 @@
...
@@ -180,16 +192,18 @@
on:click={async (e) => {
on:click={async (e) => {
e.stopPropagation();
e.stopPropagation();
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
const _function = await getFunctionById(localStorage.token, func.id).catch(
toast.error(error);
(error) => {
return null;
toast.error(error);
});
return null;
}
);
if (_
tool
) {
if (_
function
) {
let blob = new Blob([JSON.stringify([_
tool
])], {
let blob = new Blob([JSON.stringify([_
function
])], {
type: 'application/json'
type: 'application/json'
});
});
saveAs(blob, `
tool-${_tool
.id}-export-${Date.now()}.json`);
saveAs(blob, `
function-${_function
.id}-export-${Date.now()}.json`);
}
}
}}
}}
>
>
...
@@ -204,14 +218,18 @@
...
@@ -204,14 +218,18 @@
on:click={async (e) => {
on:click={async (e) => {
e.stopPropagation();
e.stopPropagation();
const res = await delete
Tool
ById(localStorage.token,
tool
.id).catch((error) => {
const res = await delete
Function
ById(localStorage.token,
func
.id).catch((error) => {
toast.error(error);
toast.error(error);
return null;
return null;
});
});
if (res) {
if (res) {
toast.success('Tool deleted successfully');
toast.success('Function deleted successfully');
tools.set(await getTools(localStorage.token));
functions = await getFunctions(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
}
}
}}
}}
>
>
...
@@ -246,7 +264,7 @@
...
@@ -246,7 +264,7 @@
<div class="flex space-x-2">
<div class="flex space-x-2">
<input
<input
id="documents-import-input"
id="documents-import-input"
bind:this={
tool
sImportInputElement}
bind:this={
function
sImportInputElement}
bind:files={importFiles}
bind:files={importFiles}
type="file"
type="file"
accept=".json"
accept=".json"
...
@@ -260,7 +278,7 @@
...
@@ -260,7 +278,7 @@
<button
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
on:click={() => {
tool
sImportInputElement.click();
function
sImportInputElement.click();
}}
}}
>
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Functions')}</div>
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Functions')}</div>
...
@@ -284,16 +302,16 @@
...
@@ -284,16 +302,16 @@
<button
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
on:click={async () => {
const _
tool
s = await export
Tool
s(localStorage.token).catch((error) => {
const _
function
s = await export
Function
s(localStorage.token).catch((error) => {
toast.error(error);
toast.error(error);
return null;
return null;
});
});
if (_
tool
s) {
if (_
function
s) {
let blob = new Blob([JSON.stringify(_
tool
s)], {
let blob = new Blob([JSON.stringify(_
function
s)], {
type: 'application/json'
type: 'application/json'
});
});
saveAs(blob, `
tool
s-export-${Date.now()}.json`);
saveAs(blob, `
function
s-export-${Date.now()}.json`);
}
}
}}
}}
>
>
...
@@ -322,18 +340,22 @@
...
@@ -322,18 +340,22 @@
on:confirm={() => {
on:confirm={() => {
const reader = new FileReader();
const reader = new FileReader();
reader.onload = async (event) => {
reader.onload = async (event) => {
const _
tool
s = JSON.parse(event.target.result);
const _
function
s = JSON.parse(event.target.result);
console.log(_
tool
s);
console.log(_
function
s);
for (const
tool
of _
tool
s) {
for (const
func
of _
function
s) {
const res = await createNew
Tool
(localStorage.token,
tool
).catch((error) => {
const res = await createNew
Function
(localStorage.token,
func
).catch((error) => {
toast.error(error);
toast.error(error);
return null;
return null;
});
});
}
}
toast.success('Tool imported successfully');
toast.success('Functions imported successfully');
tools.set(await getTools(localStorage.token));
functions = await getFunctions(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
};
};
reader.readAsText(importFiles[0]);
reader.readAsText(importFiles[0]);
...
@@ -344,8 +366,8 @@
...
@@ -344,8 +366,8 @@
<div>Please carefully review the following warnings:</div>
<div>Please carefully review the following warnings:</div>
<ul class=" mt-1 list-disc pl-4 text-xs">
<ul class=" mt-1 list-disc pl-4 text-xs">
<li>
Tools have a function calling system that
allow
s
arbitrary code execution.</li>
<li>
Functions
allow arbitrary code execution.</li>
<li>Do not install
tool
s from sources you do not fully trust.</li>
<li>Do not install
function
s from sources you do not fully trust.</li>
</ul>
</ul>
</div>
</div>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment