From 5df474abb997f0fb50493a4a0620a6546828bee3 Mon Sep 17 00:00:00 2001 From: Tristan Morris Date: Sun, 2 Feb 2025 07:58:59 -0600 Subject: [PATCH 01/45] Add support for Deepgram STT --- backend/open_webui/config.py | 6 ++ backend/open_webui/main.py | 2 + backend/open_webui/routers/audio.py | 64 +++++++++++++++++++ .../components/admin/Settings/Audio.svelte | 37 ++++++++++- 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index c37b831de..4b85d5194 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1954,6 +1954,12 @@ WHISPER_MODEL_AUTO_UPDATE = ( and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true" ) +# Add Deepgram configuration +DEEPGRAM_API_KEY = PersistentConfig( + "DEEPGRAM_API_KEY", + "audio.stt.deepgram.api_key", + os.getenv("DEEPGRAM_API_KEY", ""), +) AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( "AUDIO_STT_OPENAI_API_BASE_URL", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 00270aabc..6323f34c3 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -130,6 +130,7 @@ from open_webui.config import ( AUDIO_TTS_AZURE_SPEECH_REGION, AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, WHISPER_MODEL, + DEEPGRAM_API_KEY, WHISPER_MODEL_AUTO_UPDATE, WHISPER_MODEL_DIR, # Retrieval @@ -609,6 +610,7 @@ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE app.state.config.STT_MODEL = AUDIO_STT_MODEL app.state.config.WHISPER_MODEL = WHISPER_MODEL +app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index c1b15772b..7242042e2 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -11,6 +11,7 @@ from pydub.silence import split_on_silence import aiohttp import aiofiles import requests +import mimetypes from fastapi import ( Depends, @@ -138,6 +139,7 @@ class STTConfigForm(BaseModel): ENGINE: str MODEL: str WHISPER_MODEL: str + DEEPGRAM_API_KEY: str class AudioConfigUpdateForm(BaseModel): @@ -165,6 +167,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)): "ENGINE": request.app.state.config.STT_ENGINE, "MODEL": request.app.state.config.STT_MODEL, "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, + "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, }, } @@ -190,6 +193,7 @@ async def update_audio_config( request.app.state.config.STT_ENGINE = form_data.stt.ENGINE request.app.state.config.STT_MODEL = form_data.stt.MODEL request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL + request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY if request.app.state.config.STT_ENGINE == "": request.app.state.faster_whisper_model = set_faster_whisper_model( @@ -214,6 +218,7 @@ async def update_audio_config( "ENGINE": request.app.state.config.STT_ENGINE, "MODEL": request.app.state.config.STT_MODEL, "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, + "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, }, } @@ -521,6 +526,65 @@ def transcribe(request: Request, file_path): raise Exception(detail if detail else "Open WebUI: Server Connection Error") + elif request.app.state.config.STT_ENGINE == "deepgram": + try: + # Determine the MIME type of the file + mime, _ = mimetypes.guess_type(file_path) + if not mime: + mime = "audio/wav" # fallback to wav if undetectable + + # Read the audio file + with open(file_path, "rb") as f: + file_data = f.read() + + # Build headers and parameters + headers = { + "Authorization": f"Token {request.app.state.config.DEEPGRAM_API_KEY}", + "Content-Type": mime, + } + + # Add model if specified + params = {} + if request.app.state.config.STT_MODEL: + params["model"] = request.app.state.config.STT_MODEL + + # Make request to Deepgram API + r = requests.post( + "https://api.deepgram.com/v1/listen", + headers=headers, + params=params, + data=file_data, + ) + r.raise_for_status() + response_data = r.json() + + # Extract transcript from Deepgram response + try: + transcript = response_data["results"]["channels"][0]["alternatives"][0].get("transcript", "") + except (KeyError, IndexError) as e: + log.error(f"Malformed response from Deepgram: {str(e)}") + raise Exception("Failed to parse Deepgram response - unexpected response format") + data = {"text": transcript.strip()} + + # Save transcript + transcript_file = f"{file_dir}/{id}.json" + with open(transcript_file, "w") as f: + json.dump(data, f) + + return data + + except Exception as e: + log.exception(e) + detail = None + if r is not None: + try: + res = r.json() + if "error" in res: + detail = f"External: {res['error'].get('message', '')}" + except Exception: + detail = f"External: {e}" + raise Exception(detail if detail else "Open WebUI: Server Connection Error") + def compress_audio(file_path): if os.path.getsize(file_path) > MAX_FILE_SIZE: diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index a7a030027..f3f3f3bb6 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -39,6 +39,7 @@ let STT_ENGINE = ''; let STT_MODEL = ''; let STT_WHISPER_MODEL = ''; + let STT_DEEPGRAM_API_KEY = ''; let STT_WHISPER_MODEL_LOADING = false; @@ -103,7 +104,8 @@ OPENAI_API_KEY: STT_OPENAI_API_KEY, ENGINE: STT_ENGINE, MODEL: STT_MODEL, - WHISPER_MODEL: STT_WHISPER_MODEL + WHISPER_MODEL: STT_WHISPER_MODEL, + DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY } }); @@ -143,6 +145,7 @@ STT_ENGINE = res.stt.ENGINE; STT_MODEL = res.stt.MODEL; STT_WHISPER_MODEL = res.stt.WHISPER_MODEL; + STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY; } await getVoices(); @@ -173,6 +176,7 @@ + @@ -210,6 +214,37 @@ + {:else if STT_ENGINE === 'deepgram'} +
+
+ +
+
+ +
+ +
+
{$i18n.t('STT Model')}
+
+
+ +
+
+
+ {$i18n.t('Leave model field empty to use the default model.')} + + {$i18n.t('Click here to see available models.')} + +
+
{:else if STT_ENGINE === ''}
{$i18n.t('STT Model')}
From 98ba0c37b9862c1f893a4eb078a7f37588ebdcff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:25:26 +0000 Subject: [PATCH 02/45] build(deps-dev): bump vitest Bumps the npm_and_yarn group with 1 update in the / directory: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Updates `vitest` from 1.6.0 to 1.6.1 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.1/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 122 ++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 75 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 074ff21c1..348372928 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "engines": { "node": ">=18.13.0 <=22.x.x", @@ -1558,6 +1558,7 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -2210,7 +2211,8 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", @@ -3146,13 +3148,14 @@ "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "chai": "^4.3.10" }, "funding": { @@ -3160,12 +3163,13 @@ } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.0", + "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3178,6 +3182,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^1.0.0" }, @@ -3189,10 +3194,11 @@ } }, "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.20" }, @@ -3201,10 +3207,11 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", "dev": true, + "license": "MIT", "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", @@ -3215,10 +3222,11 @@ } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^2.2.0" }, @@ -3227,10 +3235,11 @@ } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", "dev": true, + "license": "MIT", "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", @@ -3246,6 +3255,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -3496,6 +3506,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -3895,6 +3906,7 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3972,10 +3984,11 @@ "dev": true }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -3983,7 +3996,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -4019,6 +4032,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -5135,10 +5149,11 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -5257,6 +5272,7 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -6238,6 +6254,7 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -7464,6 +7481,7 @@ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } @@ -8686,6 +8704,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -9064,6 +9083,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -9078,6 +9098,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9504,7 +9525,8 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", @@ -11247,6 +11269,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -11406,10 +11429,11 @@ "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11749,10 +11773,11 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", @@ -12166,16 +12191,17 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -12189,7 +12215,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.6.0", + "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -12204,8 +12230,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index d2c4795c6..bfad6ef6e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "type": "module", "dependencies": { From 68703951e8abbdbed469953ad9beaa22806b5da2 Mon Sep 17 00:00:00 2001 From: "M.Abdulrahman Alnaseer" <20760062+abdalrohman@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:14:40 +0300 Subject: [PATCH 03/45] feat(ui): implement domain filter list for web search settings --- backend/open_webui/routers/retrieval.py | 6 +++++ .../admin/Settings/WebSearch.svelte | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 35cea6237..434f392c3 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -392,6 +392,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, } @@ -441,6 +442,7 @@ class WebSearchConfig(BaseModel): exa_api_key: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None + domain_filter_list: Optional[List[str]] = [] class WebConfig(BaseModel): @@ -553,6 +555,9 @@ async def update_rag_config( request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( form_data.web.search.concurrent_requests ) + request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = ( + form_data.web.search.domain_filter_list + ) return { "status": True, @@ -599,6 +604,7 @@ async def update_rag_config( "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, } diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index b2accbf1d..927086d5d 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -34,6 +34,14 @@ let youtubeProxyUrl = ''; const submitHandler = async () => { + // Convert domain filter string to array before sending + if (webConfig?.search?.domain_filter_list) { + webConfig.search.domain_filter_list = webConfig.search.domain_filter_list + .split(',') + .map(domain => domain.trim()) + .filter(domain => domain.length > 0); + } + const res = await updateRAGConfig(localStorage.token, { web: webConfig, youtube: { @@ -49,6 +57,10 @@ if (res) { webConfig = res.web; + // Convert array back to comma-separated string for display + if (webConfig?.search?.domain_filter_list) { + webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', '); + } youtubeLanguage = res.youtube.language.join(','); youtubeTranslation = res.youtube.translation; @@ -334,6 +346,18 @@ />
+ +
+
+ {$i18n.t('Domain Filter List')} +
+ + +
{/if} From 34b62e71cc1b0c3d98e7bc2b9d1091e2fbf1f0d2 Mon Sep 17 00:00:00 2001 From: "D. MacAlpine" Date: Wed, 5 Feb 2025 21:31:55 -0500 Subject: [PATCH 04/45] fix: check for email claim before skipping userinfo endpoint --- backend/open_webui/utils/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 7c0c53c2d..83e0ca1d6 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -193,7 +193,7 @@ class OAuthManager: log.warning(f"OAuth callback error: {e}") raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) user_data: UserInfo = token.get("userinfo") - if not user_data: + if not user_data or "email" not in user_data: user_data: UserInfo = await client.userinfo(token=token) if not user_data: log.warning(f"OAuth callback failed, user data is missing: {token}") From c676303a55b9c78800efbd7fd70c84b4ccc356ed Mon Sep 17 00:00:00 2001 From: Rory <16675082+roryeckel@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:26:13 -0600 Subject: [PATCH 05/45] enh: automatically remove incorrect backticks before code_interpreter tags --- backend/open_webui/utils/middleware.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 06763483c..402b699c1 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1188,6 +1188,23 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") + # Separate content from ending whitespace but preserve it + original_whitespace = '' + content_stripped = content.rstrip() + if len(content) > len(content_stripped): + original_whitespace = content[len(content_stripped):] + + # Count the number of backticks to identify if we are in an opening code block + backtick_segments = content_stripped.split('```') + # Odd number of ``` segments -> the last backticks are closing a block + # Even number -> the last backticks are opening a new block + if len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0: + # The trailing backticks are opening a new block, they need to be removed or it will break the code interpreter markdown + content = content_stripped.rstrip('`').rstrip() + original_whitespace + else: + # The trailing backticks are closing a block (or there are no backticks), so it won't cause issues + content = content_stripped + original_whitespace + if output: output = html.escape(json.dumps(output)) From 74b971b88861b26c9be76cd11c068de4336187b0 Mon Sep 17 00:00:00 2001 From: Rory <16675082+roryeckel@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:38:35 -0600 Subject: [PATCH 06/45] refac: clean up solution for correcting code_interpreter backticks --- backend/open_webui/utils/middleware.py | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 402b699c1..331b850ff 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1122,6 +1122,16 @@ async def process_chat_response( }, ) + def split_content_and_whitespace(content): + content_stripped = content.rstrip() + original_whitespace = content[len(content_stripped):] if len(content) > len(content_stripped) else '' + return content_stripped, original_whitespace + + def is_opening_code_block(content): + backtick_segments = content.split('```') + # Even number of segments means the last backticks are opening a new block + return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0 + # Handle as a background task async def post_response_handler(response, events): def serialize_content_blocks(content_blocks, raw=False): @@ -1188,21 +1198,12 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") - # Separate content from ending whitespace but preserve it - original_whitespace = '' - content_stripped = content.rstrip() - if len(content) > len(content_stripped): - original_whitespace = content[len(content_stripped):] - - # Count the number of backticks to identify if we are in an opening code block - backtick_segments = content_stripped.split('```') - # Odd number of ``` segments -> the last backticks are closing a block - # Even number -> the last backticks are opening a new block - if len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0: - # The trailing backticks are opening a new block, they need to be removed or it will break the code interpreter markdown + content_stripped, original_whitespace = split_content_and_whitespace(content) + if is_opening_code_block(content_stripped): + # Remove trailing backticks that would open a new block content = content_stripped.rstrip('`').rstrip() + original_whitespace else: - # The trailing backticks are closing a block (or there are no backticks), so it won't cause issues + # Keep content as is - either closing backticks or no backticks content = content_stripped + original_whitespace if output: From fd6b0398591aa59b31879bef1f701ec18cb93fd8 Mon Sep 17 00:00:00 2001 From: Vineeth B V <37930821+vinsdragonis@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:04:14 +0530 Subject: [PATCH 07/45] Added a query method for OpenSearch vector db. - This PR aims to address the error 400: "**'OpenSearchClient' object has no attribute 'query'**". - With the implemented query() method, this issue should be resolved and allow uploaded documents to be vectorized and retrieved based on the given query. --- .../retrieval/vector/dbs/opensearch.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index b3d8b5eb8..c4732e1bc 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -113,6 +113,40 @@ class OpenSearchClient: return self._result_to_search_result(result) + def query( + self, index_name: str, filter: dict, limit: Optional[int] = None + ) -> Optional[GetResult]: + if not self.has_collection(index_name): + return None + + query_body = { + "query": { + "bool": { + "filter": [] + } + }, + "_source": ["text", "metadata"], + } + + for field, value in filter.items(): + query_body["query"]["bool"]["filter"].append({ + "term": {field: value} + }) + + size = limit if limit else 10 + + try: + result = self.client.search( + index=f"{self.index_prefix}_{index_name}", + body=query_body, + size=size + ) + + return self._result_to_get_result(result) + + except Exception as e: + return None + def get_or_create_index(self, index_name: str, dimension: int): if not self.has_index(index_name): self._create_index(index_name, dimension) From 80e123f58f1c8c807aad29489e394be38a73f393 Mon Sep 17 00:00:00 2001 From: hurxxxx Date: Thu, 6 Feb 2025 16:41:24 +0900 Subject: [PATCH 08/45] fix : O3 also does not support the max_tokens parameter, so title generation is not possible when using the O3 model --- backend/open_webui/routers/openai.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index d18f2a8ff..96ed50ceb 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -75,9 +75,9 @@ async def cleanup_response( await session.close() -def openai_o1_handler(payload): +def openai_o1_o3_handler(payload): """ - Handle O1 specific parameters + Handle o1, o3 specific parameters """ if "max_tokens" in payload: # Remove "max_tokens" from the payload @@ -621,10 +621,10 @@ async def generate_chat_completion( url = request.app.state.config.OPENAI_API_BASE_URLS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx] - # Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" - is_o1 = payload["model"].lower().startswith("o1-") - if is_o1: - payload = openai_o1_handler(payload) + # Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" + is_o1_o3 = payload["model"].lower().startswith(("o1-", "o3-")) + if is_o1_o3: + payload = openai_o1_o3_handler(payload) elif "api.openai.com" not in url: # Remove "max_completion_tokens" from the payload for backward compatibility if "max_completion_tokens" in payload: From b9480c0e8a16aee5ec0919aa973db75389ae5b54 Mon Sep 17 00:00:00 2001 From: hurxxxx Date: Thu, 6 Feb 2025 16:53:04 +0900 Subject: [PATCH 09/45] fix : o1 should also be applied --- backend/open_webui/routers/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 96ed50ceb..afda36237 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -622,7 +622,7 @@ async def generate_chat_completion( key = request.app.state.config.OPENAI_API_KEYS[idx] # Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" - is_o1_o3 = payload["model"].lower().startswith(("o1-", "o3-")) + is_o1_o3 = payload["model"].lower().startswith(("o1", "o3-")) if is_o1_o3: payload = openai_o1_o3_handler(payload) elif "api.openai.com" not in url: From 7c78facfd9ae16c69df93dcf7e8ce3e2ea5744be Mon Sep 17 00:00:00 2001 From: Vineeth B V <37930821+vinsdragonis@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:36:11 +0530 Subject: [PATCH 10/45] Update opensearch.py --- backend/open_webui/retrieval/vector/dbs/opensearch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index c4732e1bc..41d634391 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -114,9 +114,9 @@ class OpenSearchClient: return self._result_to_search_result(result) def query( - self, index_name: str, filter: dict, limit: Optional[int] = None + self, collection_name: str, filter: dict, limit: Optional[int] = None ) -> Optional[GetResult]: - if not self.has_collection(index_name): + if not self.has_collection(collection_name): return None query_body = { @@ -137,7 +137,7 @@ class OpenSearchClient: try: result = self.client.search( - index=f"{self.index_prefix}_{index_name}", + index=f"{self.index_prefix}_{collection_name}", body=query_body, size=size ) From a023667e1ee2e8ff00cc6aaa46f5106252937241 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 00:37:10 -0800 Subject: [PATCH 11/45] fix: user params save issue --- src/lib/components/chat/Settings/General.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte index ce8ed80e2..cb7843c69 100644 --- a/src/lib/components/chat/Settings/General.svelte +++ b/src/lib/components/chat/Settings/General.svelte @@ -49,6 +49,7 @@ function_calling: null, seed: null, temperature: null, + reasoning_effort: null, frequency_penalty: null, repeat_last_n: null, mirostat: null, @@ -333,9 +334,13 @@ system: system !== '' ? system : undefined, params: { stream_response: params.stream_response !== null ? params.stream_response : undefined, + function_calling: + params.function_calling !== null ? params.function_calling : undefined, seed: (params.seed !== null ? params.seed : undefined) ?? undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined, temperature: params.temperature !== null ? params.temperature : undefined, + reasoning_effort: + params.reasoning_effort !== null ? params.reasoning_effort : undefined, frequency_penalty: params.frequency_penalty !== null ? params.frequency_penalty : undefined, repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined, From feffdf197f91e1c03a60d88288a538edb1829fc6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 00:38:06 -0800 Subject: [PATCH 12/45] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0fb03537d..56ab09b05 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa In the last part of the command, replace `open-webui` with your container name if it is different. -Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/). +Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating). ### Using the Dev Branch 🌙 From a1e26016bb288464e6d2518dee71570963f4a270 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 00:52:22 -0800 Subject: [PATCH 13/45] fix: new connections config --- src/lib/components/admin/Settings/Connections.svelte | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/components/admin/Settings/Connections.svelte b/src/lib/components/admin/Settings/Connections.svelte index 35e6e0293..c7145464c 100644 --- a/src/lib/components/admin/Settings/Connections.svelte +++ b/src/lib/components/admin/Settings/Connections.svelte @@ -101,14 +101,17 @@ const addOpenAIConnectionHandler = async (connection) => { OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url]; OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key]; - OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config; + OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config; await updateOpenAIHandler(); }; const addOllamaConnectionHandler = async (connection) => { OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url]; - OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config; + OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = { + ...connection.config, + key: connection.key + }; await updateOllamaHandler(); }; From 8ca21ea83830107e5842a91487276aa414fddcab Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 01:07:01 -0800 Subject: [PATCH 14/45] refac: styling --- src/routes/(app)/admin/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte index 2d10ae64b..bc3caa338 100644 --- a/src/routes/(app)/admin/+layout.svelte +++ b/src/routes/(app)/admin/+layout.svelte @@ -26,7 +26,7 @@ {#if loaded}
From 14398ab62847c1840a9413efa0a34cf12df89dad Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 01:28:33 -0800 Subject: [PATCH 15/45] refac: styling --- .../components/chat/Messages/Citations.svelte | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/lib/components/chat/Messages/Citations.svelte b/src/lib/components/chat/Messages/Citations.svelte index b434b15db..232d8abb1 100644 --- a/src/lib/components/chat/Messages/Citations.svelte +++ b/src/lib/components/chat/Messages/Citations.svelte @@ -97,7 +97,7 @@ {#if citations.length > 0}
{#if citations.length <= 3} -
+
{#each citations as citation, idx}
{:else} - +
-
- -
+
+ +
{#each citations.slice(0, 2) as citation, idx} {/each}
-
+
{citations.length - 2} {$i18n.t('more')} @@ -167,7 +175,7 @@
-
+
{#each citations as citation, idx} From 88db4ca7babc232f9bbb2962bb078178deb038b6 Mon Sep 17 00:00:00 2001 From: binxn <78713335+binxn@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:30:27 +0100 Subject: [PATCH 16/45] Update jina_search.py Updated Jina's search function in order to use POST and make use of the result count passed by the user Note: Jina supports a max of 10 result count --- .../open_webui/retrieval/web/jina_search.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/retrieval/web/jina_search.py b/backend/open_webui/retrieval/web/jina_search.py index 3de6c1807..02af42ea6 100644 --- a/backend/open_webui/retrieval/web/jina_search.py +++ b/backend/open_webui/retrieval/web/jina_search.py @@ -20,14 +20,26 @@ def search_jina(api_key: str, query: str, count: int) -> list[SearchResult]: list[SearchResult]: A list of search results """ jina_search_endpoint = "https://s.jina.ai/" - headers = {"Accept": "application/json", "Authorization": f"Bearer {api_key}"} - url = str(URL(jina_search_endpoint + query)) - response = requests.get(url, headers=headers) + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": api_key, + "X-Retain-Images": "none" + } + + payload = { + "q": query, + "count": count if count <= 10 else 10 + } + + url = str(URL(jina_search_endpoint)) + response = requests.post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() results = [] - for result in data["data"][:count]: + for result in data["data"]: results.append( SearchResult( link=result["url"], From 8215aa36d00be0d003267a3e2f6d24a046dfc5f5 Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Thu, 6 Feb 2025 17:57:00 +0100 Subject: [PATCH 17/45] oidc: pick up username correctly --- backend/open_webui/utils/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 83e0ca1d6..b98e6e585 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -281,7 +281,7 @@ class OAuthManager: username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM name = user_data.get(username_claim) - if not isinstance(user, str): + if not isinstance(name, str): name = email role = self.get_user_role(None, user_data) From 89669a21fc4859e1a5dfb30778a7e3ce3791d0aa Mon Sep 17 00:00:00 2001 From: Xingjian Xie Date: Thu, 6 Feb 2025 23:01:43 +0000 Subject: [PATCH 18/45] Refactor common code between inlet and outlet --- backend/open_webui/utils/chat.py | 141 ++++++------------------- backend/open_webui/utils/filter.py | 100 ++++++++++++++++++ backend/open_webui/utils/middleware.py | 105 ++---------------- 3 files changed, 143 insertions(+), 203 deletions(-) create mode 100644 backend/open_webui/utils/filter.py diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 0719f6af5..ebd5bb5e3 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -44,6 +44,10 @@ from open_webui.utils.response import ( convert_response_ollama_to_openai, convert_streaming_response_ollama_to_openai, ) +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL @@ -177,116 +181,37 @@ async def chat_completed(request: Request, form_data: dict, user: Any): except Exception as e: return Exception(f"Error: {e}") - __event_emitter__ = get_event_emitter( - { - "chat_id": data["chat_id"], - "message_id": data["id"], - "session_id": data["session_id"], - "user_id": user.id, - } - ) + metadata = { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + "user_id": user.id, + } - __event_call__ = get_event_call( - { - "chat_id": data["chat_id"], - "message_id": data["id"], - "session_id": data["session_id"], - "user_id": user.id, - } - ) + extra_params = { + "__event_emitter__": get_event_emitter(metadata), + "__event_call__": get_event_call(metadata), + "__user__": { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + }, + "__metadata__": metadata, + "__request__": request, + } - def get_priority(function_id): - function = Functions.get_function_by_id(function_id) - if function is not None and hasattr(function, "valves"): - # TODO: Fix FunctionModel to include vavles - return (function.valves if function.valves else {}).get("priority", 0) - return 0 - - filter_ids = [function.id for function in Functions.get_global_filter_functions()] - if "info" in model and "meta" in model["info"]: - filter_ids.extend(model["info"]["meta"].get("filterIds", [])) - filter_ids = list(set(filter_ids)) - - enabled_filter_ids = [ - function.id - for function in Functions.get_functions_by_type("filter", active_only=True) - ] - filter_ids = [ - filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids - ] - - # Sort filter_ids by priority, using the get_priority function - filter_ids.sort(key=get_priority) - - for filter_id in filter_ids: - filter = Functions.get_function_by_id(filter_id) - if not filter: - continue - - if filter_id in request.app.state.FUNCTIONS: - function_module = request.app.state.FUNCTIONS[filter_id] - else: - function_module, _, _ = load_function_module_by_id(filter_id) - request.app.state.FUNCTIONS[filter_id] = function_module - - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(filter_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - if not hasattr(function_module, "outlet"): - continue - try: - outlet = function_module.outlet - - # Get the signature of the function - sig = inspect.signature(outlet) - params = {"body": data} - - # Extra parameters to be passed to the function - extra_params = { - "__model__": model, - "__id__": filter_id, - "__event_emitter__": __event_emitter__, - "__event_call__": __event_call__, - "__request__": request, - } - - # Add extra params in contained in function signature - for key, value in extra_params.items(): - if key in sig.parameters: - params[key] = value - - if "__user__" in sig.parameters: - __user__ = { - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - } - - try: - if hasattr(function_module, "UserValves"): - __user__["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id( - filter_id, user.id - ) - ) - except Exception as e: - print(e) - - params = {**params, "__user__": __user__} - - if inspect.iscoroutinefunction(outlet): - data = await outlet(**params) - else: - data = outlet(**params) - - except Exception as e: - return Exception(f"Error: {e}") - - return data + try: + result, _ = await process_filter_functions( + handler_type="outlet", + filter_ids=get_sorted_filter_ids(model), + request=request, + data=data, + extra_params=extra_params, + ) + return result + except Exception as e: + return Exception(f"Error: {e}") async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py new file mode 100644 index 000000000..2ad0c025e --- /dev/null +++ b/backend/open_webui/utils/filter.py @@ -0,0 +1,100 @@ +import inspect +from open_webui.utils.plugin import load_function_module_by_id +from open_webui.models.functions import Functions + +def get_sorted_filter_ids(model): + def get_priority(function_id): + function = Functions.get_function_by_id(function_id) + if function is not None and hasattr(function, "valves"): + # TODO: Fix FunctionModel to include vavles + return (function.valves if function.valves else {}).get("priority", 0) + return 0 + + filter_ids = [function.id for function in Functions.get_global_filter_functions()] + if "info" in model and "meta" in model["info"]: + filter_ids.extend(model["info"]["meta"].get("filterIds", [])) + filter_ids = list(set(filter_ids)) + + enabled_filter_ids = [ + function.id + for function in Functions.get_functions_by_type("filter", active_only=True) + ] + + filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] + filter_ids.sort(key=get_priority) + return filter_ids + +async def process_filter_functions( + handler_type, + filter_ids, + request, + data, + extra_params +): + skip_files = None + + for filter_id in filter_ids: + filter = Functions.get_function_by_id(filter_id) + if not filter: + continue + + if filter_id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[filter_id] + else: + function_module, _, _ = load_function_module_by_id(filter_id) + request.app.state.FUNCTIONS[filter_id] = function_module + + # Check if the function has a file_handler variable + if handler_type == "inlet" and hasattr(function_module, "file_handler"): + skip_files = function_module.file_handler + + # Apply valves to the function + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(filter_id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + # Prepare handler function + handler = getattr(function_module, handler_type, None) + if not handler: + continue + + try: + # Prepare parameters + sig = inspect.signature(handler) + params = {"body": data} + + # Add extra parameters that exist in the handler's signature + for key in list(extra_params.keys()): + if key in sig.parameters: + params[key] = extra_params[key] + + # Handle user parameters + if "__user__" in sig.parameters: + if hasattr(function_module, "UserValves"): + try: + params["__user__"]["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id( + filter_id, params["__user__"]["id"] + ) + ) + except Exception as e: + print(e) + + + # Execute handler + if inspect.iscoroutinefunction(handler): + data = await handler(**params) + else: + data = handler(**params) + + except Exception as e: + print(f"Error in {handler_type} handler {filter_id}: {e}") + raise e + + # Handle file cleanup for inlet + if skip_files and "files" in data.get("metadata", {}): + del data["metadata"]["files"] + + return data, {} \ No newline at end of file diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 331b850ff..c69d0c909 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -68,6 +68,10 @@ from open_webui.utils.misc import ( ) from open_webui.utils.tools import get_tools from open_webui.utils.plugin import load_function_module_by_id +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) from open_webui.tasks import create_task @@ -91,99 +95,6 @@ log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) -async def chat_completion_filter_functions_handler(request, body, model, extra_params): - skip_files = None - - def get_filter_function_ids(model): - def get_priority(function_id): - function = Functions.get_function_by_id(function_id) - if function is not None and hasattr(function, "valves"): - # TODO: Fix FunctionModel - return (function.valves if function.valves else {}).get("priority", 0) - return 0 - - filter_ids = [ - function.id for function in Functions.get_global_filter_functions() - ] - if "info" in model and "meta" in model["info"]: - filter_ids.extend(model["info"]["meta"].get("filterIds", [])) - filter_ids = list(set(filter_ids)) - - enabled_filter_ids = [ - function.id - for function in Functions.get_functions_by_type("filter", active_only=True) - ] - - filter_ids = [ - filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids - ] - - filter_ids.sort(key=get_priority) - return filter_ids - - filter_ids = get_filter_function_ids(model) - for filter_id in filter_ids: - filter = Functions.get_function_by_id(filter_id) - if not filter: - continue - - if filter_id in request.app.state.FUNCTIONS: - function_module = request.app.state.FUNCTIONS[filter_id] - else: - function_module, _, _ = load_function_module_by_id(filter_id) - request.app.state.FUNCTIONS[filter_id] = function_module - - # Check if the function has a file_handler variable - if hasattr(function_module, "file_handler"): - skip_files = function_module.file_handler - - # Apply valves to the function - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(filter_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - if hasattr(function_module, "inlet"): - try: - inlet = function_module.inlet - - # Create a dictionary of parameters to be passed to the function - params = {"body": body} | { - k: v - for k, v in { - **extra_params, - "__model__": model, - "__id__": filter_id, - }.items() - if k in inspect.signature(inlet).parameters - } - - if "__user__" in params and hasattr(function_module, "UserValves"): - try: - params["__user__"]["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id( - filter_id, params["__user__"]["id"] - ) - ) - except Exception as e: - print(e) - - if inspect.iscoroutinefunction(inlet): - body = await inlet(**params) - else: - body = inlet(**params) - - except Exception as e: - print(f"Error: {e}") - raise e - - if skip_files and "files" in body.get("metadata", {}): - del body["metadata"]["files"] - - return body, {} - - async def chat_completion_tools_handler( request: Request, body: dict, user: UserModel, models, tools ) -> tuple[dict, dict]: @@ -782,8 +693,12 @@ async def process_chat_payload(request, form_data, metadata, user, model): ) try: - form_data, flags = await chat_completion_filter_functions_handler( - request, form_data, model, extra_params + form_data, flags = await process_filter_functions( + handler_type="inlet", + filter_ids=get_sorted_filter_ids(model), + request=request, + data=form_data, + extra_params=extra_params, ) except Exception as e: raise Exception(f"Error: {e}") From 3a2d964d393008d53fe10a2994b61a3d4879ad0d Mon Sep 17 00:00:00 2001 From: EntropyYue Date: Fri, 7 Feb 2025 07:43:39 +0800 Subject: [PATCH 19/45] enh: optimize time display --- src/lib/components/common/Collapsible.svelte | 12 +++++++++--- src/lib/i18n/locales/en-US/translation.json | 1 + src/lib/i18n/locales/zh-CN/translation.json | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/components/common/Collapsible.svelte b/src/lib/components/common/Collapsible.svelte index 4f6b1ea95..4ee4c1b11 100644 --- a/src/lib/components/common/Collapsible.svelte +++ b/src/lib/components/common/Collapsible.svelte @@ -74,9 +74,15 @@
{#if attributes?.type === 'reasoning'} {#if attributes?.done === 'true' && attributes?.duration} - {$i18n.t('Thought for {{DURATION}}', { - DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() - })} + {#if attributes.duration < 60} + {$i18n.t('Thought for {{DURATION}} seconds', { + DURATION: attributes.duration + })} + {:else} + {$i18n.t('Thought for {{DURATION}}', { + DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() + })} + {/if} {:else} {$i18n.t('Thinking...')} {/if} diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index cca4e1468..2b46c3a6e 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -944,6 +944,7 @@ "This will reset the knowledge base and sync all files. Do you wish to continue?": "", "Thorough explanation": "", "Thought for {{DURATION}}": "", + "Thought for {{DURATION}} seconds": "", "Tika": "", "Tika Server URL required.": "", "Tiktoken": "", diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index 2e06bc61f..a114fd012 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -943,7 +943,8 @@ "This will delete all models including custom models and cannot be undone.": "这将删除所有模型,包括自定义模型,且无法撤销。", "This will reset the knowledge base and sync all files. Do you wish to continue?": "这将重置知识库并替换所有文件为目录下文件。确认继续?", "Thorough explanation": "解释较为详细", - "Thought for {{DURATION}}": "思考时间 {{DURATION}}", + "Thought for {{DURATION}}": "已推理 持续 {{DURATION}}", + "Thought for {{DURATION}} seconds": "已推理 持续 {{DURATION}} 秒", "Tika": "Tika", "Tika Server URL required.": "请输入 Tika 服务器地址。", "Tiktoken": "Tiktoken", From 5ef00dece96d8ccea448e32193749a26dfa328b3 Mon Sep 17 00:00:00 2001 From: Tiancong Li Date: Fri, 7 Feb 2025 19:13:51 +0800 Subject: [PATCH 20/45] i18n: update zh-TW --- src/lib/i18n/locales/zh-TW/translation.json | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index aa505a2e7..f4503ca7c 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -63,11 +63,11 @@ "Allowed Endpoints": "允許的端點", "Already have an account?": "已經有帳號了嗎?", "Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "作為 top_p 的替代方案,旨在確保質量和多樣性的平衡。相對於最可能的 token 機率而言,參數 p 代表一個 token 被考慮在内的最低機率。例如,當 p=0.05 且最可能的 token 機率為 0.9 時,數值低於 0.045 的對數機率會被過濾掉。(預設值:0.0)", - "Always": "", + "Always": "總是", "Amazing": "很棒", "an assistant": "一位助手", - "Analyzed": "", - "Analyzing...": "", + "Analyzed": "分析完畢", + "Analyzing...": "分析中……", "and": "和", "and {{COUNT}} more": "和另外 {{COUNT}} 個", "and create a new shared link.": "並建立新的共用連結。", @@ -172,11 +172,11 @@ "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "剪貼簿寫入權限遭拒。請檢查您的瀏覽器設定,授予必要的存取權限。", "Clone": "複製", "Clone Chat": "複製對話", - "Clone of {{TITLE}}": "", + "Clone of {{TITLE}}": "{{TITLE}} 的副本", "Close": "關閉", "Code execution": "程式碼執行", "Code formatted successfully": "程式碼格式化成功", - "Code Interpreter": "", + "Code Interpreter": "程式碼解釋器", "Collection": "收藏", "Color": "顏色", "ComfyUI": "ComfyUI", @@ -238,7 +238,7 @@ "Default": "預設", "Default (Open AI)": "預設 (OpenAI)", "Default (SentenceTransformers)": "預設 (SentenceTransformers)", - "Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "", + "Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "預設模式透過在執行前呼叫工具一次,來與更廣泛的模型相容。原生模式則利用模型內建的工具呼叫能力,但需要模型本身就支援此功能。", "Default Model": "預設模型", "Default model updated": "預設模型已更新", "Default Models": "預設模型", @@ -349,7 +349,7 @@ "Enter Chunk Overlap": "輸入區塊重疊", "Enter Chunk Size": "輸入區塊大小", "Enter description": "輸入描述", - "Enter Exa API Key": "", + "Enter Exa API Key": "輸入 Exa API 金鑰", "Enter Github Raw URL": "輸入 GitHub Raw URL", "Enter Google PSE API Key": "輸入 Google PSE API 金鑰", "Enter Google PSE Engine Id": "輸入 Google PSE 引擎 ID", @@ -398,14 +398,14 @@ "Error accessing Google Drive: {{error}}": "存取 Google Drive 時發生錯誤:{{error}}", "Error uploading file: {{error}}": "上傳檔案時發生錯誤:{{error}}", "Evaluations": "評估", - "Exa API Key": "", + "Exa API Key": "Exa API 金鑰", "Example: (&(objectClass=inetOrgPerson)(uid=%s))": "範例:(&(objectClass=inetOrgPerson)(uid=%s))", "Example: ALL": "範例:ALL", "Example: mail": "範例:mail", "Example: ou=users,dc=foo,dc=example": "範例:ou=users,dc=foo,dc=example", "Example: sAMAccountName or uid or userPrincipalName": "範例:sAMAccountName 或 uid 或 userPrincipalName", "Exclude": "排除", - "Execute code for analysis": "", + "Execute code for analysis": "執行程式碼以進行分析", "Experimental": "實驗性功能", "Explore the cosmos": "探索宇宙", "Export": "匯出", @@ -458,7 +458,7 @@ "Format your variables using brackets like this:": "使用方括號格式化您的變數,如下所示:", "Frequency Penalty": "頻率懲罰", "Function": "函式", - "Function Calling": "", + "Function Calling": "函式呼叫", "Function created successfully": "成功建立函式", "Function deleted successfully": "成功刪除函式", "Function Description": "函式描述", @@ -473,7 +473,7 @@ "Functions imported successfully": "成功匯入函式", "General": "一般", "General Settings": "一般設定", - "Generate an image": "", + "Generate an image": "產生圖片", "Generate Image": "產生圖片", "Generating search query": "正在產生搜尋查詢", "Get started": "開始使用", @@ -630,7 +630,7 @@ "More": "更多", "Name": "名稱", "Name your knowledge base": "命名您的知識庫", - "Native": "", + "Native": "原生", "New Chat": "新增對話", "New Folder": "新增資料夾", "New Password": "新密碼", @@ -725,7 +725,7 @@ "Please enter a prompt": "請輸入提示詞", "Please fill in all fields.": "請填寫所有欄位。", "Please select a model first.": "請先選擇型號。", - "Please select a model.": "", + "Please select a model.": "請選擇一個模型。", "Please select a reason": "請選擇原因", "Port": "連接埠", "Positive attitude": "積極的態度", @@ -734,7 +734,7 @@ "Previous 30 days": "過去 30 天", "Previous 7 days": "過去 7 天", "Profile Image": "個人檔案圖片", - "Prompt": "", + "Prompt": "提示詞", "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "提示詞(例如:告訴我關於羅馬帝國的一些趣事)", "Prompt Content": "提示詞內容", "Prompt created successfully": "提示詞建立成功", @@ -810,7 +810,7 @@ "Search options": "搜尋選項", "Search Prompts": "搜尋提示詞", "Search Result Count": "搜尋結果數量", - "Search the internet": "", + "Search the internet": "搜尋網際網路", "Search Tools": "搜尋工具", "SearchApi API Key": "SearchApi API 金鑰", "SearchApi Engine": "SearchApi 引擎", @@ -980,7 +980,7 @@ "Tools": "工具", "Tools Access": "工具存取", "Tools are a function calling system with arbitrary code execution": "工具是一個具有任意程式碼執行功能的函式呼叫系統", - "Tools Function Calling Prompt": "", + "Tools Function Calling Prompt": "工具函式呼叫提示詞", "Tools have a function calling system that allows arbitrary code execution": "工具具有允許執行任意程式碼的函式呼叫系統", "Tools have a function calling system that allows arbitrary code execution.": "工具具有允許執行任意程式碼的函式呼叫系統。", "Top K": "Top K", @@ -1051,7 +1051,7 @@ "Web Loader Settings": "網頁載入器設定", "Web Search": "網頁搜尋", "Web Search Engine": "網頁搜尋引擎", - "Web Search in Chat": "", + "Web Search in Chat": "在對話中進行網路搜尋", "Web Search Query Generation": "網頁搜尋查詢生成", "Webhook URL": "Webhook URL", "WebUI Settings": "WebUI 設定", @@ -1081,8 +1081,8 @@ "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "您可以透過下方的「管理」按鈕新增記憶,將您與大型語言模型的互動個人化,讓它們更有幫助並更符合您的需求。", "You cannot upload an empty file.": "您無法上傳空檔案", "You do not have permission to access this feature.": "您沒有權限訪問此功能", - "You do not have permission to upload files": "", - "You do not have permission to upload files.": "您沒有權限上傳檔案", + "You do not have permission to upload files": "您沒有權限上傳檔案", + "You do not have permission to upload files.": "您沒有權限上傳檔案。", "You have no archived conversations.": "您沒有已封存的對話。", "You have shared this chat": "您已分享此對話", "You're a helpful assistant.": "您是一位樂於助人的助手。", From 5ca6afc0fc853411316e6db498498243e565ab81 Mon Sep 17 00:00:00 2001 From: Patrick Deniso Date: Fri, 7 Feb 2025 12:15:54 -0500 Subject: [PATCH 21/45] add s3 key prefix support --- backend/open_webui/config.py | 1 + backend/open_webui/storage/provider.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index bf6f1d025..17f53be74 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -660,6 +660,7 @@ S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None) S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None) S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None) S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None) +S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None) S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None) GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 0c0a8aacf..60fdf77b5 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -10,6 +10,7 @@ from open_webui.config import ( S3_ACCESS_KEY_ID, S3_BUCKET_NAME, S3_ENDPOINT_URL, + S3_KEY_PREFIX, S3_REGION_NAME, S3_SECRET_ACCESS_KEY, GCS_BUCKET_NAME, @@ -98,7 +99,8 @@ class S3StorageProvider(StorageProvider): """Handles uploading of the file to S3 storage.""" _, file_path = LocalStorageProvider.upload_file(file, filename) try: - self.s3_client.upload_file(file_path, self.bucket_name, filename) + s3_key = os.path.join(S3_KEY_PREFIX, filename) + self.s3_client.upload_file(file_path, self.bucket_name, s3_key) return ( open(file_path, "rb").read(), "s3://" + self.bucket_name + "/" + filename, From 94f56db5eeb6645d5b660bc24f9fe70355419362 Mon Sep 17 00:00:00 2001 From: Mistrick Date: Sat, 8 Feb 2025 01:10:18 +0700 Subject: [PATCH 22/45] fix max seed for comfyui --- backend/open_webui/utils/images/comfyui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/images/comfyui.py b/backend/open_webui/utils/images/comfyui.py index 679fff9f6..b86c25759 100644 --- a/backend/open_webui/utils/images/comfyui.py +++ b/backend/open_webui/utils/images/comfyui.py @@ -161,7 +161,7 @@ async def comfyui_generate_image( seed = ( payload.seed if payload.seed - else random.randint(0, 18446744073709551614) + else random.randint(0, 1125899906842624) ) for node_id in node.node_ids: workflow[node_id]["inputs"][node.key] = seed From 7f8247692685ef23e54bef781a83c96c9943876a Mon Sep 17 00:00:00 2001 From: Patrick Deniso Date: Fri, 7 Feb 2025 13:56:57 -0500 Subject: [PATCH 23/45] use key_prefix in rest of S3StorageProvider --- backend/open_webui/storage/provider.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 60fdf77b5..f287daf2f 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -94,35 +94,36 @@ class S3StorageProvider(StorageProvider): aws_secret_access_key=S3_SECRET_ACCESS_KEY, ) self.bucket_name = S3_BUCKET_NAME + self.key_prefix = S3_KEY_PREFIX def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]: """Handles uploading of the file to S3 storage.""" _, file_path = LocalStorageProvider.upload_file(file, filename) try: - s3_key = os.path.join(S3_KEY_PREFIX, filename) + s3_key = os.path.join(self.key_prefix, filename) self.s3_client.upload_file(file_path, self.bucket_name, s3_key) return ( open(file_path, "rb").read(), - "s3://" + self.bucket_name + "/" + filename, + "s3://" + self.bucket_name + "/" + s3_key, ) except ClientError as e: raise RuntimeError(f"Error uploading file to S3: {e}") - + def get_file(self, file_path: str) -> str: """Handles downloading of the file from S3 storage.""" try: - bucket_name, key = file_path.split("//")[1].split("/") - local_file_path = f"{UPLOAD_DIR}/{key}" - self.s3_client.download_file(bucket_name, key, local_file_path) + s3_key = self._extract_s3_key(file_path) + local_file_path = self._get_local_file_path(s3_key) + self.s3_client.download_file(self.bucket_name, s3_key, local_file_path) return local_file_path except ClientError as e: raise RuntimeError(f"Error downloading file from S3: {e}") def delete_file(self, file_path: str) -> None: """Handles deletion of the file from S3 storage.""" - filename = file_path.split("/")[-1] try: - self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename) + s3_key = self._extract_s3_key(file_path) + self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) except ClientError as e: raise RuntimeError(f"Error deleting file from S3: {e}") @@ -135,6 +136,9 @@ class S3StorageProvider(StorageProvider): response = self.s3_client.list_objects_v2(Bucket=self.bucket_name) if "Contents" in response: for content in response["Contents"]: + # Skip objects that were not uploaded from open-webui in the first place + if not content["Key"].startswith(self.key_prefix): continue + self.s3_client.delete_object( Bucket=self.bucket_name, Key=content["Key"] ) @@ -144,6 +148,12 @@ class S3StorageProvider(StorageProvider): # Always delete from local storage LocalStorageProvider.delete_all_files() + # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name. + def _extract_s3_key(self, full_file_path: str) -> str: + return ''.join(full_file_path.split("//")[1].split("/")[1:]) + + def _get_local_file_path(self, s3_key: str) -> str: + return f"{UPLOAD_DIR}/{s3_key.split('/')[-1]}" class GCSStorageProvider(StorageProvider): def __init__(self): From c092db379e79964c15ecb8d744ce66d8f98886c9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 7 Feb 2025 11:23:04 -0800 Subject: [PATCH 24/45] fix --- backend/open_webui/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index bf6f1d025..01f91ec64 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1645,7 +1645,7 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig( # This ensures the highest level of safety and reliability of the information sources. RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST", - "rag.rag.web.search.domain.filter_list", + "rag.web.search.domain.filter_list", [ # "wikipedia.com", # "wikimedia.org", From f8a8218149d106e173cd152fd0d07479afcc17e8 Mon Sep 17 00:00:00 2001 From: Patrick Deniso Date: Fri, 7 Feb 2025 14:42:16 -0500 Subject: [PATCH 25/45] fix bug where '/' was not properly inserted in s3 key strings --- backend/open_webui/storage/provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index f287daf2f..afc50b397 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -94,7 +94,7 @@ class S3StorageProvider(StorageProvider): aws_secret_access_key=S3_SECRET_ACCESS_KEY, ) self.bucket_name = S3_BUCKET_NAME - self.key_prefix = S3_KEY_PREFIX + self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else "" def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]: """Handles uploading of the file to S3 storage.""" @@ -150,7 +150,7 @@ class S3StorageProvider(StorageProvider): # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name. def _extract_s3_key(self, full_file_path: str) -> str: - return ''.join(full_file_path.split("//")[1].split("/")[1:]) + return '/'.join(full_file_path.split("//")[1].split("/")[1:]) def _get_local_file_path(self, s3_key: str) -> str: return f"{UPLOAD_DIR}/{s3_key.split('/')[-1]}" From 4b4a86d4e7a49149de3e6daeee7fa9b79e9148e5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 7 Feb 2025 11:51:27 -0800 Subject: [PATCH 26/45] chore: dependencies --- backend/requirements.txt | 2 ++ pyproject.toml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 14ad4b9cd..92d9c7f22 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -32,6 +32,8 @@ boto3==1.35.53 argon2-cffi==23.1.0 APScheduler==3.10.4 +RestrictedPython==8.0 + # AI libraries openai anthropic diff --git a/pyproject.toml b/pyproject.toml index f121089e8..076e58b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ dependencies = [ "argon2-cffi==23.1.0", "APScheduler==3.10.4", + + "RestrictedPython==8.0", + "openai", "anthropic", "google-generativeai==0.7.2", From 85912d726e759e83a42f03497175b08a2a421a1a Mon Sep 17 00:00:00 2001 From: tarmst Date: Fri, 7 Feb 2025 19:53:25 +0000 Subject: [PATCH 27/45] Adding debug logs for oauth role & group management --- backend/open_webui/env.py | 1 + backend/open_webui/utils/oauth.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 00605e15d..0be3887f8 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -92,6 +92,7 @@ log_sources = [ "RAG", "WEBHOOK", "SOCKET", + "OAUTH", ] SRC_LOG_LEVELS = {} diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 83e0ca1d6..3adbac20e 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1,6 +1,7 @@ import base64 import logging import mimetypes +import sys import uuid import aiohttp @@ -40,7 +41,11 @@ from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.webhook import post_webhook +from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OAUTH"]) auth_manager_config = AppConfig() auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE @@ -72,12 +77,15 @@ class OAuthManager: def get_user_role(self, user, user_data): if user and Users.get_num_users() == 1: # If the user is the only user, assign the role "admin" - actually repairs role for single user on login + log.debug("Assigning the only user the admin role") return "admin" if not user and Users.get_num_users() == 0: # If there are no users, assign the role "admin", as the first user will be an admin + log.debug("Assigning the first user the admin role") return "admin" if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT: + log.debug("Running OAUTH Role management") oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES @@ -93,17 +101,24 @@ class OAuthManager: claim_data = claim_data.get(nested_claim, {}) oauth_roles = claim_data if isinstance(claim_data, list) else None + log.debug(f"Oauth Roles claim: {oauth_claim}") + log.debug(f"User roles from oauth: {oauth_roles}") + log.debug(f"Accepted user roles: {oauth_allowed_roles}") + log.debug(f"Accepted admin roles: {oauth_admin_roles}") + # If any roles are found, check if they match the allowed or admin roles if oauth_roles: # If role management is enabled, and matching roles are provided, use the roles for allowed_role in oauth_allowed_roles: # If the user has any of the allowed roles, assign the role "user" if allowed_role in oauth_roles: + log.debug("Assigned user the user role") role = "user" break for admin_role in oauth_admin_roles: # If the user has any of the admin roles, assign the role "admin" if admin_role in oauth_roles: + log.debug("Assigned user the admin role") role = "admin" break else: @@ -117,16 +132,23 @@ class OAuthManager: return role def update_user_groups(self, user, user_data, default_permissions): + log.debug("Running OAUTH Group management") oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM user_oauth_groups: list[str] = user_data.get(oauth_claim, list()) user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id) all_available_groups: list[GroupModel] = Groups.get_groups() + log.debug(f"Oauth Groups claim: {oauth_claim}") + log.debug(f"User oauth groups: {user_oauth_groups}") + log.debug(f"User's current groups: {[g.name for g in user_current_groups]}") + log.debug(f"All groups available in OpenWebUI: {[g.name for g in all_available_groups]}") + # Remove groups that user is no longer a part of for group_model in user_current_groups: if group_model.name not in user_oauth_groups: # Remove group from user + log.debug(f"Removing user from group {group_model.name} as it is no longer in their oauth groups") user_ids = group_model.user_ids user_ids = [i for i in user_ids if i != user.id] @@ -152,6 +174,7 @@ class OAuthManager: gm.name == group_model.name for gm in user_current_groups ): # Add user to group + log.debug(f"Adding user to group {group_model.name} as it was found in their oauth groups") user_ids = group_model.user_ids user_ids.append(user.id) From c56bedc5ffa81967270da44b2f76f5a7d51a7b16 Mon Sep 17 00:00:00 2001 From: Xingjian Xie Date: Fri, 7 Feb 2025 20:15:54 +0000 Subject: [PATCH 28/45] Fix tag_content_handler issue: after_tag should be remove from the current content_blocks --- backend/open_webui/utils/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 331b850ff..79210030d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1260,10 +1260,10 @@ async def process_chat_response( match.end() : ] # Content after opening tag - # Remove the start tag from the currently handling text block + # Remove the start tag and after from the currently handling text block content_blocks[-1]["content"] = content_blocks[-1][ "content" - ].replace(match.group(0), "") + ].replace(match.group(0) + after_tag, "") if before_tag: content_blocks[-1]["content"] = before_tag From 546ef6ab42c2fcc59aeae8e03bf9b67ec74f28e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Sat, 8 Feb 2025 09:49:16 +0900 Subject: [PATCH 29/45] Check is response is OK from retrieve the picture if not then default --- backend/open_webui/utils/oauth.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 83e0ca1d6..5e52937f4 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -261,15 +261,18 @@ class OAuthManager: } async with aiohttp.ClientSession() as session: async with session.get(picture_url, **get_kwargs) as resp: - picture = await resp.read() - base64_encoded_picture = base64.b64encode( - picture - ).decode("utf-8") - guessed_mime_type = mimetypes.guess_type(picture_url)[0] - if guessed_mime_type is None: - # assume JPG, browsers are tolerant enough of image formats - guessed_mime_type = "image/jpeg" - picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + if resp.ok: + picture = await resp.read() + base64_encoded_picture = base64.b64encode( + picture + ).decode("utf-8") + guessed_mime_type = mimetypes.guess_type(picture_url)[0] + if guessed_mime_type is None: + # assume JPG, browsers are tolerant enough of image formats + guessed_mime_type = "image/jpeg" + picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + else: + picture_url = "/user.png" except Exception as e: log.error( f"Error downloading profile image '{picture_url}': {e}" From d39a274ef89eb8381fc85d1775d8c167077a3a32 Mon Sep 17 00:00:00 2001 From: zoupingshi Date: Sat, 8 Feb 2025 12:14:01 +0800 Subject: [PATCH 30/45] chore: fix some typos Signed-off-by: zoupingshi --- backend/open_webui/models/chats.py | 2 +- backend/open_webui/utils/middleware.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 73ff6c102..9e0a5865e 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -470,7 +470,7 @@ class ChatTable: try: with get_db() as db: # it is possible that the shared link was deleted. hence, - # we check if the chat is still shared by checkng if a chat with the share_id exists + # we check if the chat is still shared by checking if a chat with the share_id exists chat = db.query(Chat).filter_by(share_id=id).first() if chat: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 79210030d..984359869 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -572,13 +572,13 @@ async def chat_image_generation_handler( { "type": "status", "data": { - "description": f"An error occured while generating an image", + "description": f"An error occurred while generating an image", "done": True, }, } ) - system_message_content = "Unable to generate an image, tell the user that an error occured" + system_message_content = "Unable to generate an image, tell the user that an error occurred" if system_message_content: form_data["messages"] = add_or_update_system_message( From 95aaacfeb494a659be2b488a9237e20bec18d273 Mon Sep 17 00:00:00 2001 From: SentinalMax Date: Fri, 7 Feb 2025 22:52:24 -0600 Subject: [PATCH 31/45] fixed GGUF model upload instability --- backend/open_webui/routers/ollama.py | 119 +++++++++++++++++---------- backend/open_webui/utils/misc.py | 9 +- 2 files changed, 79 insertions(+), 49 deletions(-) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 2ab06eb95..1c6365683 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -11,10 +11,8 @@ import re import time from typing import Optional, Union from urllib.parse import urlparse - import aiohttp from aiocache import cached - import requests from fastapi import ( @@ -990,6 +988,8 @@ async def generate_chat_completion( ) payload = {**form_data.model_dump(exclude_none=True)} + if "metadata" in payload: + del payload["metadata"] model_id = payload["model"] model_info = Models.get_model_by_id(model_id) @@ -1408,9 +1408,10 @@ async def download_model( return None +# TODO: Progress bar does not reflect size & duration of upload. @router.post("/models/upload") @router.post("/models/upload/{url_idx}") -def upload_model( +async def upload_model( request: Request, file: UploadFile = File(...), url_idx: Optional[int] = None, @@ -1419,62 +1420,90 @@ def upload_model( if url_idx is None: url_idx = 0 ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + file_path = os.path.join(UPLOAD_DIR, file.filename) + os.makedirs(UPLOAD_DIR, exist_ok=True) - file_path = f"{UPLOAD_DIR}/{file.filename}" + # --- P1: save file locally --- + chunk_size = 1024 * 1024 * 2 # 2 MB chunks + with open(file_path, "wb") as out_f: + while True: + chunk = file.file.read(chunk_size) + #log.info(f"Chunk: {str(chunk)}") # DEBUG + if not chunk: + break + out_f.write(chunk) - # Save file in chunks - with open(file_path, "wb+") as f: - for chunk in file.file: - f.write(chunk) - - def file_process_stream(): + async def file_process_stream(): nonlocal ollama_url total_size = os.path.getsize(file_path) - chunk_size = 1024 * 1024 + log.info(f"Total Model Size: {str(total_size)}") # DEBUG + + # --- P2: SSE progress + calculate sha256 hash --- + file_hash = calculate_sha256(file_path, chunk_size) + log.info(f"Model Hash: {str(file_hash)}") # DEBUG try: with open(file_path, "rb") as f: - total = 0 - done = False - - while not done: - chunk = f.read(chunk_size) - if not chunk: - done = True - continue - - total += len(chunk) - progress = round((total / total_size) * 100, 2) - - res = { + bytes_read = 0 + while chunk := f.read(chunk_size): + bytes_read += len(chunk) + progress = round(bytes_read / total_size * 100, 2) + data_msg = { "progress": progress, "total": total_size, - "completed": total, + "completed": bytes_read, } - yield f"data: {json.dumps(res)}\n\n" + yield f"data: {json.dumps(data_msg)}\n\n" - if done: - f.seek(0) - hashed = calculate_sha256(f) - f.seek(0) + # --- P3: Upload to ollama /api/blobs --- + with open(file_path, "rb") as f: + url = f"{ollama_url}/api/blobs/sha256:{file_hash}" + response = requests.post(url, data=f) - url = f"{ollama_url}/api/blobs/sha256:{hashed}" - response = requests.post(url, data=f) + if response.ok: + log.info(f"Uploaded to /api/blobs") # DEBUG + # Remove local file + os.remove(file_path) - if response.ok: - res = { - "done": done, - "blob": f"sha256:{hashed}", - "name": file.filename, - } - os.remove(file_path) - yield f"data: {json.dumps(res)}\n\n" - else: - raise Exception( - "Ollama: Could not create blob, Please try again." - ) + # Create model in ollama + model_name, ext = os.path.splitext(file.filename) + log.info(f"Created Model: {model_name}") # DEBUG + + create_payload = { + "model": model_name, + # Reference the file by its original name => the uploaded blob's digest + "files": { + file.filename: f"sha256:{file_hash}" + }, + } + log.info(f"Model Payload: {create_payload}") # DEBUG + + # Call ollama /api/create + #https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model + create_resp = requests.post( + url=f"{ollama_url}/api/create", + headers={"Content-Type": "application/json"}, + data=json.dumps(create_payload), + ) + + if create_resp.ok: + log.info(f"API SUCCESS!") # DEBUG + done_msg = { + "done": True, + "blob": f"sha256:{file_hash}", + "name": file.filename, + "model_created": model_name, + } + yield f"data: {json.dumps(done_msg)}\n\n" + else: + raise Exception( + f"Failed to create model in Ollama. {create_resp.text}" + ) + + else: + raise Exception("Ollama: Could not create blob, Please try again.") except Exception as e: res = {"error": str(e)} yield f"data: {json.dumps(res)}\n\n" - return StreamingResponse(file_process_stream(), media_type="text/event-stream") + return StreamingResponse(file_process_stream(), media_type="text/event-stream") \ No newline at end of file diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index b07393921..eb90ea5ea 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -244,11 +244,12 @@ def get_gravatar_url(email): return f"https://www.gravatar.com/avatar/{hash_hex}?d=mp" -def calculate_sha256(file): +def calculate_sha256(file_path, chunk_size): + #Compute SHA-256 hash of a file efficiently in chunks sha256 = hashlib.sha256() - # Read the file in chunks to efficiently handle large files - for chunk in iter(lambda: file.read(8192), b""): - sha256.update(chunk) + with open(file_path, "rb") as f: + while chunk := f.read(chunk_size): + sha256.update(chunk) return sha256.hexdigest() From 3dde2f67cfaa938b9b25cf549deb9249793835d8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 7 Feb 2025 22:57:39 -0800 Subject: [PATCH 32/45] refac --- backend/open_webui/utils/chat.py | 6 +++--- backend/open_webui/utils/filter.py | 29 ++++++++++++-------------- backend/open_webui/utils/middleware.py | 23 +++++++++++++------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index ebd5bb5e3..f0b52eca2 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -203,10 +203,10 @@ async def chat_completed(request: Request, form_data: dict, user: Any): try: result, _ = await process_filter_functions( - handler_type="outlet", - filter_ids=get_sorted_filter_ids(model), request=request, - data=data, + filter_ids=get_sorted_filter_ids(model), + filter_type="outlet", + form_data=data, extra_params=extra_params, ) return result diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 2ad0c025e..88fe70353 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -2,6 +2,7 @@ import inspect from open_webui.utils.plugin import load_function_module_by_id from open_webui.models.functions import Functions + def get_sorted_filter_ids(model): def get_priority(function_id): function = Functions.get_function_by_id(function_id) @@ -19,17 +20,14 @@ def get_sorted_filter_ids(model): function.id for function in Functions.get_functions_by_type("filter", active_only=True) ] - + filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] filter_ids.sort(key=get_priority) return filter_ids + async def process_filter_functions( - handler_type, - filter_ids, - request, - data, - extra_params + request, filter_ids, filter_type, form_data, extra_params ): skip_files = None @@ -45,7 +43,7 @@ async def process_filter_functions( request.app.state.FUNCTIONS[filter_id] = function_module # Check if the function has a file_handler variable - if handler_type == "inlet" and hasattr(function_module, "file_handler"): + if filter_type == "inlet" and hasattr(function_module, "file_handler"): skip_files = function_module.file_handler # Apply valves to the function @@ -56,14 +54,14 @@ async def process_filter_functions( ) # Prepare handler function - handler = getattr(function_module, handler_type, None) + handler = getattr(function_module, filter_type, None) if not handler: continue try: # Prepare parameters sig = inspect.signature(handler) - params = {"body": data} + params = {"body": form_data} # Add extra parameters that exist in the handler's signature for key in list(extra_params.keys()): @@ -82,19 +80,18 @@ async def process_filter_functions( except Exception as e: print(e) - # Execute handler if inspect.iscoroutinefunction(handler): - data = await handler(**params) + form_data = await handler(**params) else: - data = handler(**params) + form_data = handler(**params) except Exception as e: - print(f"Error in {handler_type} handler {filter_id}: {e}") + print(f"Error in {filter_type} handler {filter_id}: {e}") raise e # Handle file cleanup for inlet - if skip_files and "files" in data.get("metadata", {}): - del data["metadata"]["files"] + if skip_files and "files" in form_data.get("metadata", {}): + del form_data["metadata"]["files"] - return data, {} \ No newline at end of file + return form_data, {} diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index c69d0c909..14d01221c 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -694,10 +694,10 @@ async def process_chat_payload(request, form_data, metadata, user, model): try: form_data, flags = await process_filter_functions( - handler_type="inlet", - filter_ids=get_sorted_filter_ids(model), request=request, - data=form_data, + filter_ids=get_sorted_filter_ids(model), + filter_type="inlet", + form_data=form_data, extra_params=extra_params, ) except Exception as e: @@ -1039,11 +1039,15 @@ async def process_chat_response( def split_content_and_whitespace(content): content_stripped = content.rstrip() - original_whitespace = content[len(content_stripped):] if len(content) > len(content_stripped) else '' + original_whitespace = ( + content[len(content_stripped) :] + if len(content) > len(content_stripped) + else "" + ) return content_stripped, original_whitespace def is_opening_code_block(content): - backtick_segments = content.split('```') + backtick_segments = content.split("```") # Even number of segments means the last backticks are opening a new block return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0 @@ -1113,10 +1117,15 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") - content_stripped, original_whitespace = split_content_and_whitespace(content) + content_stripped, original_whitespace = ( + split_content_and_whitespace(content) + ) if is_opening_code_block(content_stripped): # Remove trailing backticks that would open a new block - content = content_stripped.rstrip('`').rstrip() + original_whitespace + content = ( + content_stripped.rstrip("`").rstrip() + + original_whitespace + ) else: # Keep content as is - either closing backticks or no backticks content = content_stripped + original_whitespace From 9be8bea6f4046ec828686ec3dc0728363d3117fc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 8 Feb 2025 01:07:05 -0800 Subject: [PATCH 33/45] fix: filter --- backend/open_webui/utils/chat.py | 1 + backend/open_webui/utils/filter.py | 14 ++++++++------ backend/open_webui/utils/middleware.py | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index f0b52eca2..3b6d5ea04 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -199,6 +199,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any): }, "__metadata__": metadata, "__request__": request, + "__model__": model, } try: diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 88fe70353..de51bd46e 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -61,12 +61,14 @@ async def process_filter_functions( try: # Prepare parameters sig = inspect.signature(handler) - params = {"body": form_data} - - # Add extra parameters that exist in the handler's signature - for key in list(extra_params.keys()): - if key in sig.parameters: - params[key] = extra_params[key] + params = {"body": form_data} | { + k: v + for k, v in { + **extra_params, + "__id__": filter_id, + }.items() + if k in sig.parameters + } # Handle user parameters if "__user__" in sig.parameters: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 39033a92a..29bfb2ba1 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -617,6 +617,7 @@ async def process_chat_payload(request, form_data, metadata, user, model): }, "__metadata__": metadata, "__request__": request, + "__model__": model, } # Initialize events to store additional event to be sent to the client From 181fca4707dda3222b29b3d6de40dad7fe3b2260 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 8 Feb 2025 02:34:30 -0800 Subject: [PATCH 34/45] refac: transformer.js --- package-lock.json | 184 +++++++++++------- package.json | 4 +- .../admin/Evaluations/Leaderboard.svelte | 4 +- vite.config.ts | 15 +- 4 files changed, 132 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47856e4c0..e5c18101b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.21", + "kokoro-js": "^1.1.1", "marked": "^9.1.0", "mermaid": "^10.9.3", "paneforge": "^0.0.6", @@ -62,7 +63,8 @@ "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "turndown": "^7.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-static-copy": "^2.2.0" }, "devDependencies": { "@sveltejs/adapter-auto": "3.2.2", @@ -1078,21 +1080,23 @@ } }, "node_modules/@huggingface/jinja": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.1.tgz", - "integrity": "sha512-SbcBWUKDQ76lzlVYOloscUk0SJjuL1LcbZsfQv/Bxxc7dwJMYuS+DAQ+HhVw6ZkTFXArejaX5HQRuCuleYwYdA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.3.tgz", + "integrity": "sha512-vQQr2JyWvVFba3Lj9es4q9vCl1sAc74fdgnEMoX8qHrXtswap9ge9uO3ONDzQB0cQ0PUyaKY2N6HaVbTBvSXvw==", + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@huggingface/transformers": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.0.0.tgz", - "integrity": "sha512-OWIPnTijAw4DQ+IFHBOrej2SDdYyykYlTtpTLCEt5MZq/e9Cb65RS2YVhdGcgbaW/6JAL3i8ZA5UhDeWGm4iRQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.3.3.tgz", + "integrity": "sha512-OcMubhBjW6u1xnp0zSt5SvCxdGHuhP2k+w2Vlm3i0vNcTJhJTZWxxYQmPBfcb7PX+Q6c43lGSzWD6tsJFwka4Q==", + "license": "Apache-2.0", "dependencies": { - "@huggingface/jinja": "^0.3.0", - "onnxruntime-node": "1.19.2", - "onnxruntime-web": "1.20.0-dev.20241016-2b8fc5529b", + "@huggingface/jinja": "^0.3.3", + "onnxruntime-node": "1.20.1", + "onnxruntime-web": "1.21.0-dev.20250206-d981b153d3", "sharp": "^0.33.5" } }, @@ -1546,6 +1550,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -1799,7 +1804,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1812,7 +1816,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -1821,7 +1824,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1857,27 +1859,32 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1886,27 +1893,32 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" }, "node_modules/@pyscript/core": { "version": "0.4.32", @@ -3426,7 +3438,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3655,7 +3666,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "engines": { "node": ">=8" }, @@ -3731,7 +3741,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -4091,7 +4100,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -4115,7 +4123,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4127,6 +4134,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -5915,7 +5923,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -5931,7 +5938,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5955,7 +5961,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -6014,7 +6019,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6053,9 +6057,10 @@ } }, "node_modules/flatbuffers": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", - "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==" + "version": "25.1.24", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.1.24.tgz", + "integrity": "sha512-Ni+KCqYquU30UEgGkrrwpbYtUcUmNuLFcQ5Xdy9DK7WUaji+AAov+Bf12FEYmu0eI15y31oD38utnBexe0cAYA==", + "license": "Apache-2.0" }, "node_modules/flatted": { "version": "3.3.1", @@ -6126,7 +6131,6 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6446,8 +6450,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -6458,7 +6461,8 @@ "node_modules/guid-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==" + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" }, "node_modules/gulp-sort": { "version": "2.0.0", @@ -6826,7 +6830,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -6875,7 +6878,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6892,7 +6894,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6934,7 +6935,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -7114,7 +7114,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -7189,6 +7188,16 @@ "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true }, + "node_modules/kokoro-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.1.1.tgz", + "integrity": "sha512-cyLO34iI8nBJXPnd3fI4fGeQGS+a6Uatg7eXNL6QS8TLSxaa30WD6Fj7/XoIZYaHg8q6d+TCrui/f74MTY2g1g==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/transformers": "^3.3.3", + "phonemizer": "^1.2.1" + } + }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -7472,9 +7481,10 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" }, "node_modules/loupe": { "version": "2.3.7", @@ -7627,7 +7637,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -8084,7 +8093,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8168,6 +8176,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "license": "MIT", "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" @@ -8180,6 +8189,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -8199,6 +8209,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -8213,6 +8224,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8227,6 +8239,7 @@ "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", "dependencies": { "glob": "^10.3.7" }, @@ -8347,7 +8360,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8451,42 +8463,46 @@ } }, "node_modules/onnxruntime-common": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.19.2.tgz", - "integrity": "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q==" + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz", + "integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==", + "license": "MIT" }, "node_modules/onnxruntime-node": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.19.2.tgz", - "integrity": "sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz", + "integrity": "sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==", "hasInstallScript": true, + "license": "MIT", "os": [ "win32", "darwin", "linux" ], "dependencies": { - "onnxruntime-common": "1.19.2", + "onnxruntime-common": "1.20.1", "tar": "^7.0.1" } }, "node_modules/onnxruntime-web": { - "version": "1.20.0-dev.20241016-2b8fc5529b", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.20.0-dev.20241016-2b8fc5529b.tgz", - "integrity": "sha512-1XovqtgqeEFtupuyzdDQo7Tqj4GRyNHzOoXjapCEo4rfH3JrXok5VtqucWfRXHPsOI5qoNxMQ9VE+drDIp6woQ==", + "version": "1.21.0-dev.20250206-d981b153d3", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0-dev.20250206-d981b153d3.tgz", + "integrity": "sha512-esDVQdRic6J44VBMFLumYvcGfioMh80ceLmzF1yheJyuLKq/Th8VT2aj42XWQst+2bcWnAhw4IKmRQaqzU8ugg==", + "license": "MIT", "dependencies": { - "flatbuffers": "^1.12.0", + "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", - "onnxruntime-common": "1.20.0-dev.20241016-2b8fc5529b", + "onnxruntime-common": "1.21.0-dev.20250206-d981b153d3", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { - "version": "1.20.0-dev.20241016-2b8fc5529b", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.0-dev.20241016-2b8fc5529b.tgz", - "integrity": "sha512-KZK8b6zCYGZFjd4ANze0pqBnqnFTS3GIVeclQpa2qseDpXrCQJfkWBixRcrZShNhm3LpFOZ8qJYFC5/qsJK9WQ==" + "version": "1.21.0-dev.20250206-d981b153d3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0-dev.20250206-d981b153d3.tgz", + "integrity": "sha512-TwaE51xV9q2y8pM61q73rbywJnusw9ivTEHAJ39GVWNZqxCoDBpe/tQkh/w9S+o/g+zS7YeeL0I/2mEWd+dgyA==", + "license": "MIT" }, "node_modules/optionator": { "version": "0.9.3", @@ -8564,7 +8580,8 @@ "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" }, "node_modules/paneforge": { "version": "0.0.6", @@ -8747,6 +8764,12 @@ "@types/estree": "*" } }, + "node_modules/phonemizer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz", + "integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==", + "license": "Apache-2.0" + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -8800,7 +8823,8 @@ "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" }, "node_modules/polyscript": { "version": "0.12.8", @@ -9335,6 +9359,7 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -9434,7 +9459,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -9556,7 +9580,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -9659,7 +9682,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -9785,7 +9807,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11153,6 +11174,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -11169,6 +11191,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -11300,7 +11323,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -11508,7 +11530,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -11795,6 +11816,24 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.2.0.tgz", + "integrity": "sha512-ytMrKdR9iWEYHbUxs6x53m+MRl4SJsOSoMu1U1+Pfg0DjPeMlsRVx3RR5jvoonineDquIue83Oq69JvNsFSU5w==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -12593,6 +12632,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } diff --git a/package.json b/package.json index 7754eacce..aa43f6a75 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.21", + "kokoro-js": "^1.1.1", "marked": "^9.1.0", "mermaid": "^10.9.3", "paneforge": "^0.0.6", @@ -105,7 +106,8 @@ "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "turndown": "^7.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-static-copy": "^2.2.0" }, "engines": { "node": ">=18.13.0 <=22.x.x", diff --git a/src/lib/components/admin/Evaluations/Leaderboard.svelte b/src/lib/components/admin/Evaluations/Leaderboard.svelte index 59f6df916..07e19e979 100644 --- a/src/lib/components/admin/Evaluations/Leaderboard.svelte +++ b/src/lib/components/admin/Evaluations/Leaderboard.svelte @@ -1,6 +1,8 @@