From baea26d9ca34e05e27ad247d3c2e90e5744a0a53 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 13 Nov 2024 03:09:46 -0800 Subject: [PATCH] wip: user groups frontend --- backend/open_webui/apps/ollama/main.py | 73 ++--- backend/open_webui/apps/openai/main.py | 39 ++- backend/open_webui/main.py | 72 ++--- src/lib/components/admin/Users/Groups.svelte | 76 ++++- .../admin/Users/Groups/GroupModal.svelte | 191 ++++++++++++ .../components/icons/UserCircleSolid.svelte | 11 + src/lib/components/icons/UsersSolid.svelte | 9 + src/lib/components/workspace/Models.svelte | 273 +++++++++--------- .../workspace/Models/ModelMenu.svelte | 117 ++++---- 9 files changed, 572 insertions(+), 289 deletions(-) create mode 100644 src/lib/components/admin/Users/Groups/GroupModal.svelte create mode 100644 src/lib/components/icons/UserCircleSolid.svelte create mode 100644 src/lib/components/icons/UsersSolid.svelte diff --git a/backend/open_webui/apps/ollama/main.py b/backend/open_webui/apps/ollama/main.py index e566e032b..fe619b49a 100644 --- a/backend/open_webui/apps/ollama/main.py +++ b/backend/open_webui/apps/ollama/main.py @@ -13,9 +13,7 @@ import requests from open_webui.apps.webui.models.models import Models from open_webui.config import ( CORS_ALLOW_ORIGIN, - ENABLE_MODEL_FILTER, ENABLE_OLLAMA_API, - MODEL_FILTER_LIST, OLLAMA_BASE_URLS, OLLAMA_API_CONFIGS, UPLOAD_DIR, @@ -66,9 +64,6 @@ app.add_middleware( app.state.config = AppConfig() -app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST - app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS @@ -339,16 +334,18 @@ async def get_ollama_tags( if url_idx is None: models = await get_all_models() - if app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user": - models["models"] = list( - filter( - lambda model: model["name"] - in app.state.config.MODEL_FILTER_LIST, - models["models"], - ) - ) - return models + # TODO: Check User Group and Filter Models + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user": + # models["models"] = list( + # filter( + # lambda model: model["name"] + # in app.state.config.MODEL_FILTER_LIST, + # models["models"], + # ) + # ) + # return models + return models else: url = app.state.config.OLLAMA_BASE_URLS[url_idx] @@ -922,12 +919,14 @@ async def generate_chat_completion( model_id = form_data.model - if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: - raise HTTPException( - status_code=403, - detail="Model not found", - ) + # TODO: Check User Group and Filter Models + # if not bypass_filter: + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: + # raise HTTPException( + # status_code=403, + # detail="Model not found", + # ) model_info = Models.get_model_by_id(model_id) @@ -1008,12 +1007,13 @@ async def generate_openai_chat_completion( model_id = completion_form.model - if app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: - raise HTTPException( - status_code=403, - detail="Model not found", - ) + # TODO: Check User Group and Filter Models + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: + # raise HTTPException( + # status_code=403, + # detail="Model not found", + # ) model_info = Models.get_model_by_id(model_id) @@ -1054,15 +1054,16 @@ async def get_openai_models( if url_idx is None: models = await get_all_models() - if app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user": - models["models"] = list( - filter( - lambda model: model["name"] - in app.state.config.MODEL_FILTER_LIST, - models["models"], - ) - ) + # TODO: Check User Group and Filter Models + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user": + # models["models"] = list( + # filter( + # lambda model: model["name"] + # in app.state.config.MODEL_FILTER_LIST, + # models["models"], + # ) + # ) return { "data": [ diff --git a/backend/open_webui/apps/openai/main.py b/backend/open_webui/apps/openai/main.py index 5a4dba62f..9e02393d9 100644 --- a/backend/open_webui/apps/openai/main.py +++ b/backend/open_webui/apps/openai/main.py @@ -11,9 +11,7 @@ from open_webui.apps.webui.models.models import Models from open_webui.config import ( CACHE_DIR, CORS_ALLOW_ORIGIN, - ENABLE_MODEL_FILTER, ENABLE_OPENAI_API, - MODEL_FILTER_LIST, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, OPENAI_API_CONFIGS, @@ -61,9 +59,6 @@ app.add_middleware( app.state.config = AppConfig() -app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST - app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS @@ -372,15 +367,18 @@ async def get_all_models(raw=False) -> dict[str, list] | list: async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)): if url_idx is None: models = await get_all_models() - if app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user": - models["data"] = list( - filter( - lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, - models["data"], - ) - ) - return models + + # TODO: Check User Group and Filter Models + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user": + # models["data"] = list( + # filter( + # lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, + # models["data"], + # ) + # ) + # return models + return models else: url = app.state.config.OPENAI_API_BASE_URLS[url_idx] @@ -492,11 +490,10 @@ async def verify_connection( @app.post("/chat/completions") -@app.post("/chat/completions/{url_idx}") async def generate_chat_completion( form_data: dict, - url_idx: Optional[int] = None, user=Depends(get_verified_user), + bypass_filter: Optional[bool] = False, ): idx = 0 payload = {**form_data} @@ -505,6 +502,16 @@ async def generate_chat_completion( del payload["metadata"] model_id = form_data.get("model") + + # TODO: Check User Group and Filter Models + # if not bypass_filter: + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: + # raise HTTPException( + # status_code=403, + # detail="Model not found", + # ) + model_info = Models.get_model_by_id(model_id) if model_info: diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index dd7504d5f..353a1198f 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -183,7 +183,10 @@ async def lifespan(app: FastAPI): app = FastAPI( - docs_url="/docs" if ENV == "dev" else None, openapi_url="/openapi.json" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan + docs_url="/docs" if ENV == "dev" else None, + openapi_url="/openapi.json" if ENV == "dev" else None, + redoc_url=None, + lifespan=lifespan, ) app.state.config = AppConfig() @@ -1081,15 +1084,16 @@ async def get_models(user=Depends(get_verified_user)): if "pipeline" not in model or model["pipeline"].get("type", None) != "filter" ] - if app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user": - models = list( - filter( - lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, - models, - ) - ) - return {"data": models} + # TODO: Check User Group and Filter Models + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user": + # models = list( + # filter( + # lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, + # models, + # ) + # ) + # return {"data": models} return {"data": models} @@ -1106,12 +1110,14 @@ async def generate_chat_completions( detail="Model not found", ) - if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER: - if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Model not found", - ) + # TODO: Check User Group and Filter Models + # if not bypass_filter: + # if app.state.config.ENABLE_MODEL_FILTER: + # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: + # raise HTTPException( + # status_code=status.HTTP_403_FORBIDDEN, + # detail="Model not found", + # ) model = app.state.MODELS[model_id] @@ -1161,14 +1167,16 @@ async def generate_chat_completions( ), "selected_model_id": selected_model_id, } + if model.get("pipe"): + # Below does not require bypass_filter because this is the only route the uses this function and it is already bypassing the filter return await generate_function_chat_completion(form_data, user=user) if model["owned_by"] == "ollama": # Using /ollama/api/chat endpoint form_data = convert_payload_openai_to_ollama(form_data) form_data = GenerateChatCompletionForm(**form_data) response = await generate_ollama_chat_completion( - form_data=form_data, user=user, bypass_filter=True + form_data=form_data, user=user, bypass_filter=bypass_filter ) if form_data.stream: response.headers["content-type"] = "text/event-stream" @@ -1179,7 +1187,9 @@ async def generate_chat_completions( else: return convert_response_ollama_to_openai(response) else: - return await generate_openai_chat_completion(form_data, user=user) + return await generate_openai_chat_completion( + form_data, user=user, bypass_filter=bypass_filter + ) @app.post("/api/chat/completed") @@ -2297,32 +2307,6 @@ async def get_app_config(request: Request): } -@app.get("/api/config/model/filter") -async def get_model_filter_config(user=Depends(get_admin_user)): - return { - "enabled": app.state.config.ENABLE_MODEL_FILTER, - "models": app.state.config.MODEL_FILTER_LIST, - } - - -class ModelFilterConfigForm(BaseModel): - enabled: bool - models: list[str] - - -@app.post("/api/config/model/filter") -async def update_model_filter_config( - form_data: ModelFilterConfigForm, user=Depends(get_admin_user) -): - app.state.config.ENABLE_MODEL_FILTER = form_data.enabled - app.state.config.MODEL_FILTER_LIST = form_data.models - - return { - "enabled": app.state.config.ENABLE_MODEL_FILTER, - "models": app.state.config.MODEL_FILTER_LIST, - } - - # TODO: webhook endpoint should be under config endpoints diff --git a/src/lib/components/admin/Users/Groups.svelte b/src/lib/components/admin/Users/Groups.svelte index adfc1066f..538d009cf 100644 --- a/src/lib/components/admin/Users/Groups.svelte +++ b/src/lib/components/admin/Users/Groups.svelte @@ -12,6 +12,12 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; import Plus from '$lib/components/icons/Plus.svelte'; + import Badge from '$lib/components/common/Badge.svelte'; + import UsersSolid from '$lib/components/icons/UsersSolid.svelte'; + import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; + import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; + import User from '$lib/components/icons/User.svelte'; + import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte'; const i18n = getContext('i18n'); @@ -38,7 +44,16 @@ if ($user?.role !== 'admin') { await goto('/'); } else { - groups = []; + groups = [ + { + name: 'Admins', + description: 'Admins have full access to all features and settings.', + permissions: { + admin: true + }, + user_ids: [1, 2, 3] + } + ]; } loaded = true; }); @@ -117,7 +132,64 @@ {:else} -
+
+
+
Group
+ +
Users
+ +
+
+ +
+ + {#each filteredGroups as group} +
+
+
+ +
+ {group.name} +
+ +
+ {group.user_ids.length} + +
+ +
+
+ +
+ +
+
+ {/each} +
{/if} + +
+ + {/if} diff --git a/src/lib/components/admin/Users/Groups/GroupModal.svelte b/src/lib/components/admin/Users/Groups/GroupModal.svelte new file mode 100644 index 000000000..61de1721d --- /dev/null +++ b/src/lib/components/admin/Users/Groups/GroupModal.svelte @@ -0,0 +1,191 @@ + + + +
+
+
+ {#if edit} + {$i18n.t('Edit User Group')} + {:else} + {$i18n.t('Add User Group')} + {/if} +
+ +
+ +
+
+
{ + e.preventDefault(); + submitHandler(); + }} + > +
+
+
+
{$i18n.t('Name')}
+ +
+ +
+
+
+ +
+
{$i18n.t('Description')}
+ +
+ +
+
+ +
+
+ +
+ {#if edit} + + {/if} + + +
+
+
+
+
+
diff --git a/src/lib/components/icons/UserCircleSolid.svelte b/src/lib/components/icons/UserCircleSolid.svelte new file mode 100644 index 000000000..9b34c0164 --- /dev/null +++ b/src/lib/components/icons/UserCircleSolid.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/components/icons/UsersSolid.svelte b/src/lib/components/icons/UsersSolid.svelte new file mode 100644 index 000000000..db939ba71 --- /dev/null +++ b/src/lib/components/icons/UsersSolid.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/src/lib/components/workspace/Models.svelte b/src/lib/components/workspace/Models.svelte index 43630f8c5..ad990838a 100644 --- a/src/lib/components/workspace/Models.svelte +++ b/src/lib/components/workspace/Models.svelte @@ -411,7 +411,7 @@
- {#if shiftKey} + {#if $user?.role === 'admin' && shiftKey} {:else} - - - - - + + + + + {/if} { shareModelHandler(model); @@ -532,131 +535,133 @@ {/each}
-
-
- { - console.log(importFiles); +{#if $user?.role === 'admin'} +
+
+ { + console.log(importFiles); - let reader = new FileReader(); - reader.onload = async (event) => { - let savedModels = JSON.parse(event.target.result); - console.log(savedModels); + let reader = new FileReader(); + reader.onload = async (event) => { + let savedModels = JSON.parse(event.target.result); + console.log(savedModels); - for (const model of savedModels) { - if (model?.info ?? false) { - if ($models.find((m) => m.id === model.id)) { - await updateModelById(localStorage.token, model.id, model.info).catch((error) => { - return null; - }); - } else { - await addNewModel(localStorage.token, model.info).catch((error) => { - return null; - }); + for (const model of savedModels) { + if (model?.info ?? false) { + if ($models.find((m) => m.id === model.id)) { + await updateModelById(localStorage.token, model.id, model.info).catch((error) => { + return null; + }); + } else { + await addNewModel(localStorage.token, model.info).catch((error) => { + return null; + }); + } } } - } - await models.set(await getModels(localStorage.token)); - _models = $models; - }; + await models.set(await getModels(localStorage.token)); + _models = $models; + }; - reader.readAsText(importFiles[0]); - }} - /> + reader.readAsText(importFiles[0]); + }} + /> - +
+ + + +
+ - -
- - {#if localModelfiles.length > 0} -
-
- {localModelfiles.length} Local Modelfiles Detected -
- -
- -
+
+ + + +
+
- {/if} -
+ + {#if localModelfiles.length > 0} +
+
+ {localModelfiles.length} Local Modelfiles Detected +
+ +
+ +
+
+ {/if} +
+{/if} {#if $config?.features.enable_community_sharing}
diff --git a/src/lib/components/workspace/Models/ModelMenu.svelte b/src/lib/components/workspace/Models/ModelMenu.svelte index 49cefec1a..8a93fc76f 100644 --- a/src/lib/components/workspace/Models/ModelMenu.svelte +++ b/src/lib/components/workspace/Models/ModelMenu.svelte @@ -16,6 +16,7 @@ const i18n = getContext('i18n'); + export let user; export let model; export let shareHandler: Function; @@ -82,68 +83,70 @@
{$i18n.t('Export')}
- { - moveToTopHandler(); - }} - > - + {#if user?.role === 'admin'} + { + moveToTopHandler(); + }} + > + -
{$i18n.t('Move to Top')}
-
+
{$i18n.t('Move to Top')}
+
- { - hideHandler(); - }} - > - {#if model?.info?.meta?.hidden ?? false} - - - - {:else} - - - - - {/if} - -
+ { + hideHandler(); + }} + > {#if model?.info?.meta?.hidden ?? false} - {$i18n.t('Show Model')} + + + {:else} - {$i18n.t('Hide Model')} + + + + {/if} -
-
+ +
+ {#if model?.info?.meta?.hidden ?? false} + {$i18n.t('Show Model')} + {:else} + {$i18n.t('Hide Model')} + {/if} +
+ + {/if}