From ff7a54653aeca29a253239185d5f3983b83aa2b4 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 20 Nov 2025 15:34:15 -0500 Subject: [PATCH 01/22] refac/enh: unregisterServiceWorkers on update --- backend/open_webui/env.py | 3 +++ backend/open_webui/main.py | 2 ++ src/lib/apis/index.ts | 2 +- src/lib/stores/index.ts | 3 +++ src/routes/+layout.svelte | 43 ++++++++++++++++++++++++++++++++------ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 7059780220..ecad336855 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -135,6 +135,9 @@ else: PACKAGE_DATA = {"version": "0.0.0"} VERSION = PACKAGE_DATA["version"] + + +DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "") INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4())) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index e21dd5d1ed..e0e23390af 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -454,6 +454,7 @@ from open_webui.env import ( SAFE_MODE, SRC_LOG_LEVELS, VERSION, + DEPLOYMENT_ID, INSTANCE_ID, WEBUI_BUILD_HASH, WEBUI_SECRET_KEY, @@ -1983,6 +1984,7 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): async def get_app_version(): return { "version": VERSION, + "deployment_id": DEPLOYMENT_ID, } diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 126c59ad2f..e865e9ba0e 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -1425,7 +1425,7 @@ export const getVersion = async (token: string) => { throw error; } - return res?.version ?? null; + return res; }; export const getVersionUpdates = async (token: string) => { diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 5d8cc62513..c4c2dc10c9 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -8,7 +8,10 @@ import emojiShortCodes from '$lib/emoji-shortcodes.json'; // Backend export const WEBUI_NAME = writable(APP_NAME); + export const WEBUI_VERSION = writable(null); +export const WEBUI_DEPLOYMENT_ID = writable(null); + export const config: Writable = writable(undefined); export const user: Writable = writable(undefined); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a4376c4710..3f880b7cad 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -16,6 +16,7 @@ theme, WEBUI_NAME, WEBUI_VERSION, + WEBUI_DEPLOYMENT_ID, mobile, socket, chatId, @@ -54,9 +55,24 @@ import Spinner from '$lib/components/common/Spinner.svelte'; import { getUserSettings } from '$lib/apis/users'; + const unregisterServiceWorkers = async () => { + if ('serviceWorker' in navigator) { + try { + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map((r) => r.unregister())); + return true; + } catch (error) { + console.error('Error unregistering service workers:', error); + return false; + } + } + return false; + }; + // handle frontend updates (https://svelte.dev/docs/kit/configuration#version) - beforeNavigate(({ willUnload, to }) => { + beforeNavigate(async ({ willUnload, to }) => { if (updated.current && !willUnload && to?.url) { + await unregisterServiceWorkers(); location.href = to.url.href; } }); @@ -90,15 +106,30 @@ _socket.on('connect', async () => { console.log('connected', _socket.id); - const version = await getVersion(localStorage.token); - if (version !== null) { - if ($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) { + const res = await getVersion(localStorage.token); + + const deploymentId = res?.deployment_id ?? null; + const version = res?.version ?? null; + + if (version !== null || deploymentId !== null) { + if ( + ($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) || + ($WEBUI_DEPLOYMENT_ID !== null && deploymentId !== $WEBUI_DEPLOYMENT_ID) + ) { + await unregisterServiceWorkers(); location.href = location.href; - } else { - WEBUI_VERSION.set(version); + return; } } + if (deploymentId !== null) { + WEBUI_DEPLOYMENT_ID.set(deploymentId); + } + + if (version !== null) { + WEBUI_VERSION.set(version); + } + console.log('version', version); if (localStorage.getItem('token')) { From 22e85df4485f3636c7bebcff1da27e69d5b4c7d5 Mon Sep 17 00:00:00 2001 From: Blake <80996688+BlakeTnr@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:26:36 -0500 Subject: [PATCH 02/22] Support folder drag-n-drop (#19320) --- .../workspace/Knowledge/KnowledgeBase.svelte | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 0054eb2964..0121c37d0a 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -546,14 +546,40 @@ e.preventDefault(); dragged = false; + const handleUploadingFileFolder = (items) => { + for(const item of items) { + + if(item.isFile) { + item.file((file) => { + uploadFileHandler(file); + }) + continue; + } + + // Not sure why you have to call webkitGetAsEntry and isDirectory seperate, but it won't work if you try item.webkitGetAsEntry().isDirectory + const wkentry = item.webkitGetAsEntry(); + const isDirectory = wkentry.isDirectory; + if(isDirectory) { + // Read the directory + wkentry.createReader().readEntries((entries) => { + handleUploadingFileFolder(entries) + }, (error) => { + console.error('Error reading directory entries:', error); + }); + } else { + toast.info($i18n.t('Uploading file...')); + uploadFileHandler(item.getAsFile()); + toast.success($i18n.t('File uploaded!')); + } + } + } + if (e.dataTransfer?.types?.includes('Files')) { if (e.dataTransfer?.files) { - const inputFiles = e.dataTransfer?.files; + const inputItems = e.dataTransfer?.items; - if (inputFiles && inputFiles.length > 0) { - for (const file of inputFiles) { - await uploadFileHandler(file); - } + if (inputItems && inputItems.length > 0) { + handleUploadingFileFolder(inputItems) } else { toast.error($i18n.t(`File not found.`)); } From 485896753d319be6d90059286f4140e149ab1a1f Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:43:22 +0100 Subject: [PATCH 03/22] feat: Add user header information for TTS/STT requests (#93) (#19323) Resolves #19312 Co-authored-by: Claude --- backend/open_webui/routers/audio.py | 41 ++++++++++++++--------------- backend/open_webui/routers/files.py | 2 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index 45b4f1e692..1edf31fa9c 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -35,6 +35,7 @@ from pydantic import BaseModel from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import include_user_info_headers from open_webui.config import ( WHISPER_MODEL_AUTO_UPDATE, WHISPER_MODEL_DIR, @@ -364,23 +365,17 @@ async def speech(request: Request, user=Depends(get_verified_user)): **(request.app.state.config.TTS_OPENAI_PARAMS or {}), } + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}", + } + if ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + r = await session.post( url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", json=payload, - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}", - **( - { - "X-OpenWebUI-User-Name": quote(user.name, safe=" "), - "X-OpenWebUI-User-Id": user.id, - "X-OpenWebUI-User-Email": user.email, - "X-OpenWebUI-User-Role": user.role, - } - if ENABLE_FORWARD_USER_INFO_HEADERS - else {} - ), - }, + headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) @@ -570,7 +565,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): return FileResponse(file_path) -def transcription_handler(request, file_path, metadata): +def transcription_handler(request, file_path, metadata, user=None): filename = os.path.basename(file_path) file_dir = os.path.dirname(file_path) id = filename.split(".")[0] @@ -621,11 +616,15 @@ def transcription_handler(request, file_path, metadata): if language: payload["language"] = language + headers = { + "Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}" + } + if user and ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + r = requests.post( url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions", - headers={ - "Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}" - }, + headers=headers, files={"file": (filename, open(file_path, "rb"))}, data=payload, ) @@ -1027,7 +1026,7 @@ def transcription_handler(request, file_path, metadata): ) -def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None): +def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None, user=None): log.info(f"transcribe: {file_path} {metadata}") if is_audio_conversion_required(file_path): @@ -1054,7 +1053,7 @@ def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None with ThreadPoolExecutor() as executor: # Submit tasks for each chunk_path futures = [ - executor.submit(transcription_handler, request, chunk_path, metadata) + executor.submit(transcription_handler, request, chunk_path, metadata, user) for chunk_path in chunk_paths ] # Gather results as they complete @@ -1189,7 +1188,7 @@ def transcription( if language: metadata = {"language": language} - result = transcribe(request, file_path, metadata) + result = transcribe(request, file_path, metadata, user) return { **result, diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 2a5c3e5bb1..54084941fe 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -102,7 +102,7 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us ) ): file_path = Storage.get_file(file_path) - result = transcribe(request, file_path, file_metadata) + result = transcribe(request, file_path, file_metadata, user) process_file( request, From 60839606556922a4f565f405752e20ff400e24e3 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 20 Nov 2025 17:10:12 -0500 Subject: [PATCH 04/22] refac: feedback list optimisation --- backend/open_webui/models/feedbacks.py | 88 +++++++++++++- backend/open_webui/routers/evaluations.py | 56 ++++----- src/lib/apis/evaluations/index.ts | 39 ++++++ src/lib/components/admin/Evaluations.svelte | 4 +- .../admin/Evaluations/Feedbacks.svelte | 114 +++++++----------- 5 files changed, 202 insertions(+), 99 deletions(-) diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 215e36aa24..33f7f6179a 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -4,7 +4,7 @@ import uuid from typing import Optional from open_webui.internal.db import Base, get_db -from open_webui.models.chats import Chats +from open_webui.models.users import User from open_webui.env import SRC_LOG_LEVELS from pydantic import BaseModel, ConfigDict @@ -92,6 +92,28 @@ class FeedbackForm(BaseModel): model_config = ConfigDict(extra="allow") +class UserResponse(BaseModel): + id: str + name: str + email: str + role: str = "pending" + + last_active_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +class FeedbackUserResponse(FeedbackResponse): + user: Optional[UserResponse] = None + + +class FeedbackListResponse(BaseModel): + items: list[FeedbackUserResponse] + total: int + + class FeedbackTable: def insert_new_feedback( self, user_id: str, form_data: FeedbackForm @@ -143,6 +165,70 @@ class FeedbackTable: except Exception: return None + def get_feedback_items( + self, filter: dict = {}, skip: int = 0, limit: int = 30 + ) -> FeedbackListResponse: + with get_db() as db: + query = db.query(Feedback, User).join(User, Feedback.user_id == User.id) + + if filter: + order_by = filter.get("order_by") + direction = filter.get("direction") + + if order_by == "username": + if direction == "asc": + query = query.order_by(User.name.asc()) + else: + query = query.order_by(User.name.desc()) + elif order_by == "model_id": + # it's stored in feedback.data['model_id'] + if direction == "asc": + query = query.order_by( + Feedback.data["model_id"].as_string().asc() + ) + else: + query = query.order_by( + Feedback.data["model_id"].as_string().desc() + ) + elif order_by == "rating": + # it's stored in feedback.data['rating'] + if direction == "asc": + query = query.order_by( + Feedback.data["rating"].as_string().asc() + ) + else: + query = query.order_by( + Feedback.data["rating"].as_string().desc() + ) + elif order_by == "updated_at": + if direction == "asc": + query = query.order_by(Feedback.updated_at.asc()) + else: + query = query.order_by(Feedback.updated_at.desc()) + + else: + query = query.order_by(Feedback.created_at.desc()) + + # Count BEFORE pagination + total = query.count() + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + items = query.all() + + feedbacks = [] + for feedback, user in items: + feedback_model = FeedbackModel.model_validate(feedback) + user_model = UserResponse.model_validate(user) + feedbacks.append( + FeedbackUserResponse(**feedback_model.model_dump(), user=user_model) + ) + + return FeedbackListResponse(items=feedbacks, total=total) + def get_all_feedbacks(self) -> list[FeedbackModel]: with get_db() as db: return [ diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index c76a1f6915..3e5e14801c 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -7,6 +7,8 @@ from open_webui.models.feedbacks import ( FeedbackModel, FeedbackResponse, FeedbackForm, + FeedbackUserResponse, + FeedbackListResponse, Feedbacks, ) @@ -56,35 +58,10 @@ async def update_config( } -class UserResponse(BaseModel): - id: str - name: str - email: str - role: str = "pending" - - last_active_at: int # timestamp in epoch - updated_at: int # timestamp in epoch - created_at: int # timestamp in epoch - - -class FeedbackUserResponse(FeedbackResponse): - user: Optional[UserResponse] = None - - -@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse]) +@router.get("/feedbacks/all", response_model=list[FeedbackResponse]) async def get_all_feedbacks(user=Depends(get_admin_user)): feedbacks = Feedbacks.get_all_feedbacks() - - feedback_list = [] - for feedback in feedbacks: - user = Users.get_user_by_id(feedback.user_id) - feedback_list.append( - FeedbackUserResponse( - **feedback.model_dump(), - user=UserResponse(**user.model_dump()) if user else None, - ) - ) - return feedback_list + return feedbacks @router.delete("/feedbacks/all") @@ -111,6 +88,31 @@ async def delete_feedbacks(user=Depends(get_verified_user)): return success +PAGE_ITEM_COUNT = 30 + + +@router.get("/feedbacks/list", response_model=FeedbackListResponse) +async def get_feedbacks( + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_admin_user), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit) + return result + + @router.post("/feedback", response_model=FeedbackModel) async def create_feedback( request: Request, diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts index 96a689fcb1..1f48c7bfbf 100644 --- a/src/lib/apis/evaluations/index.ts +++ b/src/lib/apis/evaluations/index.ts @@ -93,6 +93,45 @@ export const getAllFeedbacks = async (token: string = '') => { return res; }; +export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (orderBy) searchParams.append('order_by', orderBy); + if (direction) searchParams.append('direction', direction); + if (page) searchParams.append('page', page.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/feedbacks/list?${searchParams.toString()}`, + { + 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 exportAllFeedbacks = async (token: string = '') => { let error = null; diff --git a/src/lib/components/admin/Evaluations.svelte b/src/lib/components/admin/Evaluations.svelte index d29dee746c..e2849bd98b 100644 --- a/src/lib/components/admin/Evaluations.svelte +++ b/src/lib/components/admin/Evaluations.svelte @@ -33,7 +33,9 @@ let feedbacks = []; onMount(async () => { + // TODO: feedbacks elo rating calculation should be done in the backend; remove below line later feedbacks = await getAllFeedbacks(localStorage.token); + loaded = true; const containerElement = document.getElementById('users-tabs-container'); @@ -117,7 +119,7 @@ {#if selectedTab === 'leaderboard'} {:else if selectedTab === 'feedbacks'} - + {/if} diff --git a/src/lib/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index 62304088ed..4b95b802fe 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -10,7 +10,7 @@ import { onMount, getContext } from 'svelte'; const i18n = getContext('i18n'); - import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations'; + import { deleteFeedbackById, exportAllFeedbacks, getFeedbackItems } from '$lib/apis/evaluations'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Download from '$lib/components/icons/Download.svelte'; @@ -23,78 +23,24 @@ import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; - import { WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { config } from '$lib/stores'; - export let feedbacks = []; - let page = 1; - $: paginatedFeedbacks = sortedFeedbacks.slice((page - 1) * 10, page * 10); + let items = null; + let total = null; let orderBy: string = 'updated_at'; let direction: 'asc' | 'desc' = 'desc'; - type Feedback = { - id: string; - data: { - rating: number; - model_id: string; - sibling_model_ids: string[] | null; - reason: string; - comment: string; - tags: string[]; - }; - user: { - name: string; - profile_image_url: string; - }; - updated_at: number; - }; - - type ModelStats = { - rating: number; - won: number; - lost: number; - }; - - function setSortKey(key: string) { + const setSortKey = (key) => { if (orderBy === key) { direction = direction === 'asc' ? 'desc' : 'asc'; } else { orderBy = key; - if (key === 'user' || key === 'model_id') { - direction = 'asc'; - } else { - direction = 'desc'; - } + direction = 'asc'; } - page = 1; - } - - $: sortedFeedbacks = [...feedbacks].sort((a, b) => { - let aVal, bVal; - - switch (orderBy) { - case 'user': - aVal = a.user?.name || ''; - bVal = b.user?.name || ''; - return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); - case 'model_id': - aVal = a.data.model_id || ''; - bVal = b.data.model_id || ''; - return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); - case 'rating': - aVal = a.data.rating; - bVal = b.data.rating; - return direction === 'asc' ? aVal - bVal : bVal - aVal; - case 'updated_at': - aVal = a.updated_at; - bVal = b.updated_at; - return direction === 'asc' ? aVal - bVal : bVal - aVal; - default: - return 0; - } - }); + }; let showFeedbackModal = false; let selectedFeedback = null; @@ -115,13 +61,41 @@ // ////////////////////// + const getFeedbacks = async () => { + try { + const res = await getFeedbackItems(localStorage.token, orderBy, direction, page).catch( + (error) => { + toast.error(`${error}`); + return null; + } + ); + + if (res) { + items = res.items; + total = res.total; + } + } catch (err) { + console.error(err); + } + }; + + $: if (page) { + getFeedbacks(); + } + + $: if (orderBy && direction) { + getFeedbacks(); + } + const deleteFeedbackHandler = async (feedbackId: string) => { const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => { toast.error(err); return null; }); if (response) { - feedbacks = feedbacks.filter((f) => f.id !== feedbackId); + toast.success($i18n.t('Feedback deleted successfully')); + page = 1; + getFeedbacks(); } }; @@ -175,10 +149,10 @@
- {feedbacks.length} + {total}
- {#if feedbacks.length > 0} + {#if total > 0}