Compare commits

...
1437 Commits
Author SHA1 Message Date
Tim Jaeryang Baek 0cef844168 Merge pull request #13758 from open-webui/dev
0.6.9
2025-05-10 23:04:48 +04:00
Timothy Jaeryang Baek c56d6963b0 doc: changelog 2025-05-10 23:01:48 +04:00
Timothy Jaeryang Baek 92e2cfdd2f refac 2025-05-10 23:00:53 +04:00
Timothy Jaeryang Baek 3ad5a7d97c chore: bump 2025-05-10 22:48:26 +04:00
Timothy Jaeryang Baek 867fddb821 chore: format 2025-05-10 22:45:58 +04:00
Timothy Jaeryang Baek de69c08a1d enh: alert private task model 2025-05-10 22:43:53 +04:00
Timothy Jaeryang Baek 30cc51ec82 feat: edit message files 2025-05-10 22:27:39 +04:00
Timothy Jaeryang Baek ba69c751ca refac: temporary chat behaviour 2025-05-10 22:12:36 +04:00
Timothy Jaeryang Baek be85f7cc35 fix: confirm dialog focus 2025-05-10 22:09:05 +04:00
Tim Jaeryang Baek 4191ec692e Merge pull request #13751 from open-webui/dev
fix: external reranker
2025-05-10 19:33:59 +04:00
Timothy Jaeryang Baek 6b5f99bf66 fix: external reranker 2025-05-10 19:33:34 +04:00
Tim Jaeryang Baek ef301aa16b Merge pull request #13707 from open-webui/dev
0.6.8
2025-05-10 19:31:52 +04:00
Timothy Jaeryang Baek 32b27749ef Update CHANGELOG.md 2025-05-10 19:29:02 +04:00
Timothy Jaeryang Baek b81a5833c7 doc: changelog 2025-05-10 19:25:42 +04:00
Timothy Jaeryang Baek 6f635d8b7d refac 2025-05-10 19:16:09 +04:00
Timothy Jaeryang Baek 549989e9ec refac 2025-05-10 19:04:40 +04:00
Timothy Jaeryang Baek 1c665592e4 chore: bump 2025-05-10 19:01:12 +04:00
Timothy Jaeryang Baek c61790b355 chore: format 2025-05-10 19:00:01 +04:00
Timothy Jaeryang Baek 3a79840a87 feat: unstylized pdf export 2025-05-10 18:59:04 +04:00
Timothy Jaeryang Baek a515a5df1a refac 2025-05-10 18:38:30 +04:00
Timothy Jaeryang Baek ba72d4625f refac 2025-05-10 18:36:45 +04:00
Timothy Jaeryang Baek 3dc34c2402 feat: external reranker settings ui 2025-05-10 18:33:52 +04:00
Timothy Jaeryang Baek be912f1529 refac 2025-05-10 18:29:04 +04:00
Timothy Jaeryang BaekandBrendan Campbell d5fd3b3600 feat: external reranker
Co-Authored-By: Brendan Campbell <20541191+bcambs09@users.noreply.github.com>
2025-05-10 18:25:20 +04:00
Timothy Jaeryang BaekandMabeck 34ec10a78c refac: web search performance
Co-Authored-By: Mabeck <64421281+mmabeck@users.noreply.github.com>
2025-05-10 17:54:41 +04:00
Timothy Jaeryang Baek 015ac2f532 refac: user list loading indicator 2025-05-10 17:23:17 +04:00
Tim Jaeryang Baek 030b6b26af Merge pull request #13735 from Classic298/dev
Fix: Markdown in Knowledgebases rendered correctly now
2025-05-10 17:13:16 +04:00
Tim Jaeryang Baek 6d1c18e118 Merge pull request #13736 from taylorwilsdon/ideas/13702
enh: Improve visibility of overflow / hidden tools when more than 3 are present
2025-05-10 17:10:45 +04:00
Tim Jaeryang Baek 54dda08d39 Merge pull request #13712 from PVBLIC-F/dev
perf - Pinecone.py
2025-05-10 17:10:08 +04:00
Tim Jaeryang Baek 7bfe4c6a43 Merge pull request #13738 from dataway/dev
fix: allow non-root jupyter base_url
2025-05-10 17:09:09 +04:00
Timothy Jaeryang Baek cc1dc6cb78 refac: config 2025-05-10 17:08:09 +04:00
PVBLIC Foundation 3f58a17e47 Update pinecone.py
•	Removed the unused Pinecone REST‐client import; we now only import ServerlessSpec and the gRPC client.
	•	Enhanced close()
	•	Call self.client.close() to explicitly shut down the underlying gRPC channel.
	•	Log success or a warning on failure.
	•	Still tear down the thread‐pool executor afterward.
	•	Context‐manager support
	•	Added __enter__()/__exit__() so you can do:

with PineconeClient() as client:
    client.insert(...)
# automatically calls client.close()
2025-05-10 06:07:27 -07:00
Tim Jaeryang Baek 3e1be8b958 Merge pull request #13746 from ghscience/dev
i18n Updated Italian Language Translation Added missing translations
2025-05-10 17:05:49 +04:00
gh d704b8b686 i18n Updated Italian Language Translation 2025-05-10 09:53:52 +00:00
PVBLIC Foundation 12c2138982 Update pinecone.py
Refactor and added debug
2025-05-09 18:15:22 -07:00
Anthony Uk 337ece4d50 code_interpreter.py: allow non-root jupyter base_url 2025-05-09 20:32:01 +02:00
Taylor Wilsdon 1299c11650 only show tools toggle if overflow present 2025-05-09 10:30:45 -04:00
Taylor Wilsdon 628a10e34a make the collapse toggle more subtle & compact 2025-05-09 10:26:48 -04:00
Classic298 513895c2d5 Update KnowledgeBase.svelte 2025-05-09 16:24:07 +02:00
Taylor Wilsdon ccbc46fbbe implement visible scrollbar for tools overflow, add show all toggle 2025-05-09 10:20:17 -04:00
Tim Jaeryang Baek 8acc1ab425 Merge pull request #13731 from tth37/fix_duplicate_web_search_urls
fix: Duplicate web search urls
2025-05-09 16:43:11 +04:00
Tim Jaeryang Baek 616e9e2760 Merge pull request #13730 from Classic298/dev
feat: Increase default users per page in admin panel to 30
2025-05-09 16:42:29 +04:00
tth37 c95a65a4bd fix: Duplicate web search urls 2025-05-09 20:06:35 +08:00
Classic298 220d35ebcb Increase users shown per page to 30 2025-05-09 13:00:42 +02:00
Classic298 947f5e840e Increased PAGE_ITEM_COUNT to 30 2025-05-09 12:59:50 +02:00
Timothy Jaeryang Baek ebf5cb22ab refac: socket 2025-05-09 14:23:16 +04:00
Timothy Jaeryang Baek d84a3c0bf4 revert: websocket 2025-05-09 14:20:43 +04:00
Timothy Jaeryang Baek 9143716463 refac: chat pdf rendering 2025-05-09 13:56:53 +04:00
Timothy Jaeryang Baek 9657474a8d refac: websocket 2025-05-09 12:45:53 +04:00
PVBLIC Foundation b38711a581 Update pinecone.py 2025-05-08 16:02:47 -07:00
PVBLIC Foundation 04b9065f08 Update pinecone.py
Now supports batched insert, upsert, and delete operations using a default batch size of 100, reducing API strain and improving throughput. All blocking calls to the Pinecone API are wrapped in asyncio.to_thread(...), ensuring async safety and preventing event loop blocking. The implementation includes zero-vector handling for efficient metadata-only queries, normalized cosine distance scores for accurate ranking, and protections against empty input operations. Logs for batch durations have been streamlined to minimize noise, while preserving key info-level success logs.
2025-05-08 15:53:30 -07:00
Timothy Jaeryang Baek 827326e1a2 refac: audio transcription issue 2025-05-08 22:57:48 +04:00
Timothy Jaeryang Baek bfa5550cc3 refac: openai already supports webm audio 2025-05-08 22:44:32 +04:00
Tim Jaeryang Baek e60fa7927d Merge pull request #13604 from Acters/dev
feat: Improve Default Code Interpreter Prompt
2025-05-08 22:14:29 +04:00
Tim Jaeryang Baek 1fea4f794f Merge pull request #13670 from HarrisonConsulting/fix/milvus-standalone-index
fix: enhance MilvusClient with dynamic index type and improved logging
2025-05-08 22:08:23 +04:00
Timothy Jaeryang Baek 7c8597765f refac/fix: ldap issue 2025-05-08 22:06:44 +04:00
Timothy Jaeryang Baek 08b6ea0e17 refac 2025-05-08 20:47:41 +04:00
Tim Jaeryang Baek 68f5ff540c Merge pull request #13695 from itk-dev/8908-focus-trap-modal
feat: focus trap modal
2025-05-08 20:44:36 +04:00
Tim Jaeryang Baek 9509528deb Merge pull request #13703 from hurxxxx/fix/onedrive-org-picker-error
fix: Add SharePoint tenant ID to OneDrive settings
2025-05-08 20:41:06 +04:00
hurxxxx 4ca0ea1e55 Add SharePoint tenant ID to OneDrive settings 2025-05-09 00:35:23 +09:00
Sine Jespersen 3886dee1ce add whitespace and semi colon 2025-05-08 13:29:24 +02:00
Sine Jespersen 305435b4ef use npm package focus-trap instead of npm package focus-trap-svelte 2025-05-08 13:22:47 +02:00
Matt Harrison 4abe2cfa7b fix: Specified "HNSW" for backwards compatibility prefering cluster environments over standalone. 2025-05-08 07:21:34 -04:00
Tim Jaeryang Baek a6d6c928d9 Merge pull request #13567 from tth37/fix_katex_chinese_delimiters
fix(katex): Allow CJK characters adjacent to math delimiters
2025-05-08 14:41:16 +04:00
Timothy Jaeryang Baek af518cacbf refac: filter out images 2025-05-08 12:43:13 +04:00
Tim Jaeryang Baek 54e4c1e0cc Merge pull request #13675 from feddersen-group/task/remove_files_from_tasks
perf: remove files from tasks
2025-05-08 12:38:39 +04:00
Tim Jaeryang Baek f6b20ca109 Merge pull request #13677 from silentoplayz/dev
refactor: Improve Default Title Generation Prompt Template for robust JSON output
2025-05-08 12:38:05 +04:00
silentoplayz 994f17b430 Update config.py
Update DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
2025-05-08 07:31:51 +00:00
Alexander Grimm a655bb5a63 skip checks in title gen 2025-05-08 06:55:43 +00:00
Alexander Grimm cf5201aada remove files from message for tasks 2025-05-08 06:50:21 +00:00
Matt Harrison 2df9f7fb4d fix: remove import for os module in milvus.py 2025-05-08 00:28:24 -04:00
Matt Harrison 731251d11a refac: streamline Milvus index type handling using configuration options 2025-05-07 23:39:56 -04:00
Matt Harrison 2b8be86f23 feat: add Milvus configuration options for index and metric types 2025-05-07 23:32:58 -04:00
Matt Harrison 5e46c27806 refac: enhance MilvusClient with dynamic index type and improved logging 2025-05-07 21:51:28 -04:00
Timothy Jaeryang Baek b34401a087 refac/fix: notes hide hidden models 2025-05-07 23:55:20 +04:00
Timothy Jaeryang Baek 61a9d9927d refac/fix: message input rtl issue 2025-05-07 23:14:42 +04:00
Timothy Jaeryang Baek f761d2cf90 refac: allow html video controls 2025-05-07 23:06:00 +04:00
Timothy Jaeryang Baek 0f40cb0de4 feat: Vazirmatn font for Persian and Arabic languages 2025-05-07 22:31:59 +04:00
Timothy Jaeryang Baek b50dcb1862 refac: remove duplicate urls 2025-05-07 22:25:18 +04:00
Tim Jaeryang Baek b9f55a44b7 Merge pull request #13346 from dh4340/patch-1
Fix: Use dynamic Python command to run uvicorn and support pyenv setups.
2025-05-07 22:08:35 +04:00
Tim Jaeryang Baek f1bb43707e Merge pull request #13566 from itk-dev/8908-accessibility-enhancements
feat: Accessibility enhancements
2025-05-07 22:07:57 +04:00
Tim Jaeryang Baek 519bb64238 Merge pull request #13623 from panda44312/patch-15
i18n: zh-cn
2025-05-07 22:06:54 +04:00
Tim Jaeryang Baek 410af53eca Merge pull request #13581 from kaytwo/refreshOauthPfp
feat: refresh oauth profile picture
2025-05-07 22:06:43 +04:00
Tim Jaeryang Baek c2de7efa2f Merge pull request #13637 from mMabeck/my-dev
i18n: Update Danish translations (da-DK)
2025-05-07 22:02:29 +04:00
Tim Jaeryang Baek 6424c8b644 Merge pull request #13643 from suleimanelkhoury/S3-tagging-boto3
fix: Add S3_ENABLE_TAGGING env variable and fix S3_USE_ACCELERATE_ENDPOINT Type Error
2025-05-07 22:02:14 +04:00
Sine Jespersen bd2021e8e1 add aria modal true and role dialog to modal 2025-05-07 15:17:11 +02:00
Suleiman Elkhoury ccc64ac6b7 Update provider.py
Add S3_ENABLE_TAGGING to add tags optionally based on the env file.
2025-05-07 14:48:49 +02:00
Suleiman Elkhoury 7620a6bf8c Update config.py
Fixed S3_USE_ACCELERATE_ENDPOINT variable from "False" to "false" since boto3 expects a Python bool, not a string.
Added S3_ENABLE_TAGGING env variable to avoid incompatiblilty with Storages that does not support tagging (like Cloudflare R2)
2025-05-07 14:46:58 +02:00
Sine Jespersen 54b30e0b7f use svelte-focus-trap in modal 2025-05-07 14:21:56 +02:00
Sine Jespersen f56983632f add npm package svelte-focus-trap 2025-05-07 14:20:06 +02:00
Mabeck 3a553f7208 Updated danish translations 2025-05-07 14:06:30 +02:00
Panda 571e4c9600 Merge branch 'dev' into patch-15 2025-05-07 11:18:13 +02:00
Timothy Jaeryang Baek a38f3c7617 refac 2025-05-07 13:10:24 +04:00
Timothy Jaeryang Baek b1fb298bee enh: contributions stats script 2025-05-07 13:09:41 +04:00
Timothy Jaeryang Baek 176a7fc986 refac 2025-05-07 12:52:55 +04:00
Tim Jaeryang Baek efd50c3c5a Merge pull request #13607 from ssakar/dev
feat: add presence_penalty parameter to openai (Qwen3)
2025-05-07 12:48:23 +04:00
Sine Jespersen 263086aa91 break and indent divs 2025-05-07 09:21:04 +02:00
Tim Jaeryang Baek a3bb7df610 Merge pull request #13530 from open-webui/dev
0.6.7
2025-05-07 03:08:25 +04:00
Timothy Jaeryang Baek 3a6a05cef1 doc: changelog 2025-05-07 03:05:49 +04:00
Serkan Sakar d62353c140 enh: add presence_penalty parameter to openai 2025-05-07 00:56:20 +02:00
Timothy Jaeryang Baek 803b39b00c refac 2025-05-07 02:45:00 +04:00
Timothy Jaeryang Baek ed5de96d1d fix: tool servers ui settings can be bypassed with search 2025-05-07 02:41:33 +04:00
Timothy Jaeryang Baek c667d27c38 refac 2025-05-07 02:37:23 +04:00
acters e088f1ef23 Merge branch 'open-webui:dev' into dev 2025-05-06 16:17:54 -06:00
Timothy Jaeryang Baek ad1486c8bd refac 2025-05-07 02:06:18 +04:00
Timothy Jaeryang Baek 6359cb55fe chore: format 2025-05-07 02:01:03 +04:00
Timothy Jaeryang Baek 96618967bd refac 2025-05-07 02:00:10 +04:00
Tim Jaeryang Baek 19bf7289c1 Merge pull request #13603 from TiancongLx/dev
i18n: complete empty values and **fix a typo** in zh-TW
2025-05-07 01:53:16 +04:00
Timothy Jaeryang Baek 369f461691 refac 2025-05-07 01:48:54 +04:00
acters 9c760ace80 Improve Default Code Interpreter Prompt
The change to the default "Code Interpreter" prompt makes it more clear to the LLM to not wrap the code in markdown's code block formatting. This improves the reliability of having code generated in the XML tags that will run properly.
2025-05-06 15:36:17 -06:00
Tiancong Li 45e9d05240 i18n: update zh-TW 2025-05-07 05:16:44 +08:00
Timothy Jaeryang Baek ace3303ff5 enh: TOOL_SERVER_CONNECTIONS env var support 2025-05-07 01:11:28 +04:00
Timothy Jaeryang Baek 62e57a4cf5 refac: auth endpoint 2025-05-07 00:54:53 +04:00
Tim Jaeryang Baek 2a4dfc02a2 Merge pull request #13540 from NoMoreFood/dev
feat: Azure TTS Allow Base URL
2025-05-07 00:49:57 +04:00
Tim Jaeryang Baek e0e151f4f4 Merge pull request #13583 from torisetxd/torisetxd-image-patch
Fix: Fix Image parameter for Automatic1111 `/generations` endpoint
2025-05-07 00:45:08 +04:00
Tim Jaeryang Baek ea07e242f5 Merge pull request #13528 from Classic298/dev
feat: Enhance YouTube Transcription Loader for multi-language support
2025-05-07 00:44:45 +04:00
Timothy Jaeryang Baek 53f2ab1676 chore: wording 2025-05-06 20:15:09 +04:00
Chris Kanich 500f4d73e1 refresh oauth profile picture 2025-05-06 11:00:35 -05:00
toriset 6c957f2407 Fixed mis-use of set_image_model in automatic1111 Image generation
A custom model parameter would always throw an error.. :/
2025-05-06 18:17:52 +03:00
Classic298 1dcbec71ec Update youtube.py 2025-05-06 17:14:00 +02:00
Classic298 87dcbd198c Update youtube.py 2025-05-06 17:11:03 +02:00
Classic298 d7927506f1 Update youtube.py 2025-05-06 17:06:21 +02:00
Classic298 f65dc715f9 Update youtube.py 2025-05-06 16:30:18 +02:00
Classic298 c69278c13c Update youtube.py 2025-05-06 16:24:27 +02:00
Classic298 a129e0954e Update youtube.py 2025-05-06 16:22:40 +02:00
Classic298 5e1cb76b93 Update youtube.py 2025-05-06 16:16:58 +02:00
tth37 8e14372dd8 enh: Support more languages 2025-05-06 21:26:09 +08:00
Sine Jespersen 9d8d4393a6 remove alt text from logo, as this is decorative and provides no value for assistive tech users 2025-05-06 14:37:35 +02:00
Sine Jespersen c4a3a9c616 fix text-contrast from 3.71 to 7.55 on dark mode 2025-05-06 14:37:05 +02:00
Sine Jespersen 8c6f4f6579 change divs to labels, as this is what they semantically are 2025-05-06 14:33:26 +02:00
Sine Jespersen 86ce111edd Add aria-labelledby to icon-button to make assistive tech able to give info on what user can expect from button 2025-05-06 14:30:01 +02:00
Sine Jespersen 0889dd3a51 add close label to close button in changelog modal, only accessible for screen readers 2025-05-06 14:24:19 +02:00
tth37 de182ddec2 fix(katex): Allow Chinese characters adjacent to math delimiters 2025-05-06 18:38:42 +08:00
Panda bfdcc12cdc i18n: zh-cn 2025-05-06 08:30:28 +02:00
Tim Jaeryang Baek ed210517c2 Merge pull request #13533 from TheRauch1/main
feat: handle user OAuth groups as list or string
2025-05-06 09:57:14 +04:00
Tim Jaeryang Baek 7b5247810c Merge pull request #13542 from tuzkiyoung/main
fix: support gpt-image-1 with correct parameter
2025-05-06 09:56:48 +04:00
杨方亮 9cc00afc6b fix: support gpt-image-1 with correct parameter 2025-05-06 10:17:22 +08:00
Bryan Berns 5c055b73a3 Add Custom Azure TTS URL - Formatting 2025-05-05 22:17:18 -04:00
Bryan Berns 853adad894 Add Custom Azure TTS URL - Fix Spaces 2025-05-05 22:12:32 -04:00
Bryan Berns 5aabe21cbe Add Custom Azure TTS URL 2025-05-05 22:08:48 -04:00
Timothy Jaeryang Baek e63b8b3879 refac 2025-05-06 00:46:32 +04:00
Tim Jaeryang Baek 0a8199a056 Merge pull request #13522 from salbahra/bug/fix-missing-api-config
fix: Correct missing `api_config` for embedding Ollama proxy endpoints
2025-05-05 23:49:40 +04:00
Timothy Jaeryang Baek 7b36466c1c refac: audio transcribe supported filetype 2025-05-05 23:42:56 +04:00
Timothy Jaeryang Baek 27da31dc83 fix: tikaloader extract images 2025-05-05 23:40:34 +04:00
therauch1 c36acd46cc enh: handle user OAuth groups as list or string 2025-05-05 21:38:31 +02:00
Tim Jaeryang Baek 413297b5c7 Merge pull request #13532 from Classic298/main
i18n: Update de-DE translation
2025-05-05 23:37:54 +04:00
Classic298 f94965909c Update translation.json 2025-05-05 21:33:45 +02:00
Timothy Jaeryang Baek 982c84c8ac fix: non admin user notes 2025-05-05 23:31:37 +04:00
Timothy Jaeryang Baek 1f45b9df2e fix: notes non admin issue 2025-05-05 23:29:40 +04:00
Tim Jaeryang Baek 3dd2e80dec Merge pull request #13525 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-05-05 23:28:04 +04:00
Classic298 67a612fe24 Update youtube.py 2025-05-05 20:40:48 +02:00
Classic298 791dd24ace Update youtube.py 2025-05-05 20:08:25 +02:00
Classic298 9cf3381381 Update youtube.py 2025-05-05 20:07:52 +02:00
Classic298 b0d74a59f1 Update youtube.py 2025-05-05 20:07:37 +02:00
Classic298 1a30b3746e Update youtube.py 2025-05-05 20:03:00 +02:00
Classic298 0a3817ed86 Update youtube.py 2025-05-05 20:00:10 +02:00
Classic298 0a845db8ec Update youtube.py 2025-05-05 19:57:21 +02:00
Classic298 7680ac2517 Update youtube.py 2025-05-05 19:57:06 +02:00
Kylapaallikko b3eb3b3c05 Update fi-FI translation.json 2025-05-05 20:21:23 +03:00
Samer Albahra d5e866d839 Fix missing api_config for embedding Ollama proxy endpoints 2025-05-05 12:33:07 -04:00
Timothy Jaeryang Baek db0a0b395f fix: oauth 2025-05-05 19:38:36 +04:00
Tim Jaeryang Baek 23b9354cf6 Merge pull request #12932 from open-webui/dev
0.6.6
2025-05-05 17:58:52 +04:00
Timothy Jaeryang Baek 4fa2e136bf refac: styling 2025-05-05 17:58:40 +04:00
Timothy Jaeryang Baek 512d475ecc refac: api usage 2025-05-05 17:43:51 +04:00
Timothy Jaeryang Baek 56cefde161 doc: changelog 2025-05-05 17:13:09 +04:00
Timothy Jaeryang Baek f78811b253 chore: bump 2025-05-05 16:49:21 +04:00
Timothy Jaeryang Baek e6ca7a414b refac 2025-05-05 16:41:51 +04:00
Tim Jaeryang Baek 367f6fb8f1 Merge pull request #13498 from omahs/main
fix: typos
2025-05-05 16:27:33 +04:00
omahs 37e0313c83 Merge branch 'dev' into main 2025-05-05 14:26:40 +02:00
Timothy Jaeryang Baek 267889809d chore: format 2025-05-05 16:19:05 +04:00
Timothy Jaeryang Baek 33c206c15c enh: include profile image during user import 2025-05-05 16:17:20 +04:00
omahs 863f227be9 fix: typos 2025-05-05 14:14:59 +02:00
Timothy Jaeryang Baek abdde10b74 refac 2025-05-05 16:12:41 +04:00
Timothy Jaeryang Baek 26e9cd0b4b enh: user role update confirm dialog 2025-05-05 16:10:36 +04:00
Timothy Jaeryang Baek 457a0f318a refac 2025-05-05 16:05:39 +04:00
Timothy Jaeryang Baek 3771d27a52 refac 2025-05-05 15:50:09 +04:00
Timothy Jaeryang Baek d16ff54895 refac 2025-05-05 15:46:55 +04:00
Timothy Jaeryang Baek de0f52b686 doc: readme 2025-05-05 15:42:14 +04:00
Timothy Jaeryang Baek eec7a2afa1 refac 2025-05-05 11:25:00 +04:00
Timothy Jaeryang Baek 9e1400f055 enh: generate title 2025-05-05 10:36:01 +04:00
Timothy Jaeryang Baek aa374823ce refac 2025-05-05 10:16:34 +04:00
Timothy Jaeryang Baek a20a920ca4 refac: styling 2025-05-05 09:41:25 +04:00
Timothy Jaeryang Baek 827fd6606c refac 2025-05-05 09:36:34 +04:00
Timothy Jaeryang Baek deb1f6b6d7 feat: ai enhanced notes 2025-05-05 09:22:54 +04:00
Tim Jaeryang Baek 0f1df43bb2 Merge pull request #13485 from TiancongLx/dev
i18n: update zh-TW
2025-05-05 08:25:24 +04:00
Tiancong Li 62a5b14642 i18n: update zh-TW 2025-05-05 10:15:20 +08:00
Timothy Jaeryang Baek 9faf2d9849 refac: styling 2025-05-05 01:51:51 +04:00
Timothy Jaeryang Baek f24d0085c2 refac 2025-05-05 01:45:55 +04:00
Timothy Jaeryang Baek 463ac99e39 enh: note versioning 2025-05-05 01:35:43 +04:00
Timothy Jaeryang Baek e8b23ef0c1 refac 2025-05-05 00:05:06 +04:00
Timothy Jaeryang Baek a6a7ce6b68 enh: import md notes 2025-05-04 23:04:15 +04:00
Timothy Jaeryang Baek 8514669053 refac 2025-05-04 18:59:36 +04:00
Timothy Jaeryang Baek 291834ecbe chore: format 2025-05-04 18:32:56 +04:00
Timothy Jaeryang Baek 360b5b303a enh: note editor 2025-05-04 18:32:21 +04:00
Timothy Jaeryang Baek d8e9b000e3 enh: upload audio 2025-05-04 18:26:33 +04:00
Timothy Jaeryang Baek 62366ad020 refac: audio recording 2025-05-04 18:17:35 +04:00
Timothy Jaeryang Baek 2946f5fcce refac: styling 2025-05-04 17:33:15 +04:00
Timothy Jaeryang Baek f2cf21d64c enh: file item display audio 2025-05-04 17:29:59 +04:00
Timothy Jaeryang Baek 6475ae3b11 refac 2025-05-04 17:23:42 +04:00
Timothy Jaeryang Baek 84a05bec7b enh: notes user group permission 2025-05-04 17:22:51 +04:00
Timothy Jaeryang Baek f716968009 enh: delete notes 2025-05-04 17:01:12 +04:00
Timothy Jaeryang Baek 18149483f2 refac: note pdf export 2025-05-04 16:59:41 +04:00
Timothy Jaeryang Baek 175bbe27e9 refac 2025-05-04 16:53:48 +04:00
Tim Jaeryang Baek d1eaa9d4ee Merge pull request #13478 from SadmL/dev
[i18n] Russian locale update
2025-05-04 02:24:38 -07:00
SadmL 9b133a2245 misspell fix 2025-05-04 12:05:21 +03:00
SadmL 0e51b75654 Russian locale update 2025-05-04 11:59:13 +03:00
Timothy Jaeryang Baek 1cf9843468 refac 2025-05-04 12:02:10 +04:00
Timothy Jaeryang Baek dd42a10aa1 refac 2025-05-04 12:01:20 +04:00
Tim Jaeryang Baek 60c0aa36a2 Merge pull request #13470 from TiancongLx/dev
i18n: update zh-TW
2025-05-04 00:41:02 -07:00
Tiancong Li a7cbfeec9f i18n: update zh-TW 2025-05-04 11:58:20 +08:00
Timothy Jaeryang Baek c5c98591f9 refac 2025-05-04 00:20:30 +04:00
Timothy Jaeryang Baek 59e52add63 chore: bump 2025-05-04 00:15:46 +04:00
Timothy Jaeryang Baek 9c4a931d22 feat: capture display media audio 2025-05-04 00:14:19 +04:00
Timothy Jaeryang Baek 4cfb99248d chore: format 2025-05-03 23:48:24 +04:00
Timothy Jaeryang Baek aefd5d9557 chore: format 2025-05-03 23:48:12 +04:00
Timothy Jaeryang Baek 0f14f34dbe refac 2025-05-03 23:47:41 +04:00
Timothy Jaeryang Baek d74809853d enh: note files 2025-05-03 23:29:19 +04:00
Timothy Jaeryang Baek 23da63b06a refac: styling 2025-05-03 23:08:28 +04:00
Timothy Jaeryang Baek 116b61c53b refac 2025-05-03 23:06:46 +04:00
Timothy Jaeryang Baek f43cb87556 refac 2025-05-03 23:05:25 +04:00
Tim Jaeryang Baek 1af45435eb Merge pull request #13459 from open-webui/notes
feat: notes
2025-05-03 11:54:04 -07:00
Timothy Jaeryang Baek 4acd278624 refac 2025-05-03 22:53:23 +04:00
Timothy Jaeryang Baek 1d82a1c8c4 feat: note editor 2025-05-03 21:18:19 +04:00
Timothy Jaeryang Baek 52d32e8bf2 refac: styling 2025-05-03 21:02:21 +04:00
Timothy Jaeryang Baek a74297ed47 refac: styling 2025-05-03 18:56:27 +04:00
Timothy Jaeryang Baek 7de6112c5b feat: note list ui 2025-05-03 18:52:13 +04:00
Tim Jaeryang Baek bfacab5a0c Merge pull request #13447 from athoik/dev
feat(ocr): add support for Docling OCR engine and language configuration
2025-05-03 07:18:05 -07:00
Tim Jaeryang Baek bbc1ae0942 Merge pull request #13441 from salbahra/bug/fix-prefix-for-embedding-model
fix: Incorrect model reference with Ollama proxy when prefixes are used
2025-05-03 07:17:40 -07:00
Timothy Jaeryang Baek 7fee84c06e feat: notes 2025-05-03 18:16:32 +04:00
Athanasios Oikonomou 437804a2f8 fix: update validation logic for Docling OCR engine and language requirements
Both Docling OCR Engine and Language(s) must be provided or both left empty.
2025-05-03 08:12:58 +03:00
Athanasios Oikonomou 4801430ad2 fix: correct condition for Docling OCR engine and language validation
Both must have value or both must be empty.
2025-05-03 08:02:00 +03:00
Athanasios Oikonomou 657162e96d feat(ocr): add support for Docling OCR engine and language configuration
This commit adds support for configuring the OCR engine and language(s) for Docling.
Configuration can be set via the environment variables `DOCLING_OCR_ENGINE` and `DOCLING_OCR_LANG`, or through the UI.

Fixes #13133
2025-05-03 00:32:06 +03:00
Samer Albahra cafba3eaff Fix model reference with Ollama proxy when prefixes are used 2025-05-02 11:30:59 -04:00
Tim Jaeryang Baek 7d184c3a14 Merge pull request #13085 from ayan4m1/fix/tika-image-ocr
fix: pass extractInlineImages header to Tika if PDF_EXTRACT_IMAGES is true
2025-05-02 03:47:51 -07:00
Timothy Jaeryang Baek 754f631a07 feat: OAUTH_BLOCKED_GROUPS support 2025-05-02 14:47:02 +04:00
Timothy Jaeryang Baek c1ddb00377 refac: styling 2025-05-02 14:19:53 +04:00
Timothy Jaeryang Baek 74f160b849 refac 2025-05-02 14:16:11 +04:00
Tim Jaeryang Baek 11d599c55d Merge pull request #13111 from tth37/perf_multi_thread_web_searching
perf: Concurrent processing for web search queries
2025-05-02 03:15:49 -07:00
Tim Jaeryang Baek 7b014e44ee Merge pull request #13376 from Thaniel94/add-whisper-language-constraint
feat: Added WHISPER_LANGUAGE env variable
2025-05-02 03:08:00 -07:00
Timothy Jaeryang Baek 4a0b2a3afc chore: bump 2025-05-02 14:06:57 +04:00
Tim Jaeryang Baek 2d5643fb17 Merge pull request #13405 from yak1ex/fix_multiple_actions_in_an_action_module_do_not_work_as_intended
fix: Multiple actions in an action module do not work as intended.
2025-05-02 03:06:05 -07:00
Tim Jaeryang Baek d522914774 Merge pull request #13413 from hurxxxx/fix/eslint-version-mismatch
fix: eslint version mismatch
2025-05-02 03:05:30 -07:00
Tim Jaeryang Baek 3013ce2d1a Merge pull request #13415 from TiancongLx/dev
i18n: update zh-TW
2025-05-02 03:04:39 -07:00
Timothy Jaeryang Baek 6d81eef425 refac: reasoning detection 2025-05-02 14:04:12 +04:00
Timothy Jaeryang Baek c258272709 refac 2025-05-02 13:37:41 +04:00
Tim Jaeryang Baek a0981e01a0 Merge pull request #13417 from flefevre/dev-i8n-french
i18n french
2025-05-02 02:28:11 -07:00
hurxxxx 10243b0fac chore: bump @typescript-eslint/eslint-plugin to 8.31.1 2025-05-02 09:44:51 +09:00
nathaniel cc14aacaaa Improvements to parsing of WHISPER_LANGUAGE environment variable (Setting as empty string now equivalent to unsetting/removing WHISPER_LANGUAGE). Resolved crash caused when unset 2025-05-01 22:03:49 +01:00
nathaniel ef7acfbf3d WHISPER_LANGUAGE no longer a "PersistentConfig" variable (Was not appropriate with how WHISPER_LANGUAGE is currently configured). 2025-05-01 21:33:57 +01:00
flefevre fb8a7e8549 Merge branch 'dev' into dev-i8n-french 2025-05-01 20:17:50 +02:00
Francois LE FEVRE 754a3c4cb9 last elements to translate into french 2025-05-01 20:07:56 +02:00
Tiancong Li 725761ad6b i18n: update zh-TW 2025-05-01 22:56:47 +08:00
Francois LE FEVRE 57cb030a24 seconde review on french translation 2025-05-01 15:59:52 +02:00
Francois LE FEVRE 12626107b6 first elements translated in french 2025-05-01 15:46:58 +02:00
hurxxxx 72f985aba7 fix: align eslint plugin and parser versions 2025-05-01 22:39:29 +09:00
Timothy Jaeryang Baek 5d5e351937 feat: notes scaffolding 2025-05-01 16:39:36 +04:00
Tim Jaeryang Baek 2062d3434e Merge pull request #13385 from open-webui/dependabot/npm_and_yarn/dev/typescript-eslint/parser-8.31.1
build(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 8.31.1
2025-05-01 04:59:00 -07:00
Timothy Jaeryang Baek f26cafd984 chore: langchain bump 2025-05-01 15:58:45 +04:00
Tim Jaeryang Baek aca82d64a0 Merge pull request #13391 from open-webui/dependabot/pip/backend/dev/rapidocr-onnxruntime-1.4.4
build(deps): bump rapidocr-onnxruntime from 1.3.24 to 1.4.4 in /backend
2025-05-01 04:56:32 -07:00
Tim Jaeryang Baek 3715919a6b Merge pull request #13389 from open-webui/dependabot/npm_and_yarn/dev/codemirror-lang-hcl-0.1.0
build(deps): bump codemirror-lang-hcl from 0.0.0-beta.2 to 0.1.0
2025-05-01 04:55:52 -07:00
Tim Jaeryang Baek 0e3b9f9da1 Merge pull request #13388 from open-webui/dependabot/npm_and_yarn/dev/prosemirror-schema-list-1.5.1
build(deps): bump prosemirror-schema-list from 1.4.1 to 1.5.1
2025-05-01 04:55:45 -07:00
Tim Jaeryang Baek f10e7ac81b Merge pull request #13387 from open-webui/dependabot/npm_and_yarn/dev/tiptap/core-2.11.9
build(deps): bump @tiptap/core from 2.10.0 to 2.11.9
2025-05-01 04:55:36 -07:00
Tim Jaeryang Baek 61580e9490 Merge pull request #13404 from NoMoreFood/dev
fix: Use SHA256 For Query Result Computation
2025-05-01 04:55:16 -07:00
Tim Jaeryang Baek 6f2d61dc4f Merge pull request #13398 from hurxxxx/i18n-korean
i18n: Update Korean translation.json and parsed messages
2025-05-01 04:48:56 -07:00
Bryan Berns 32257089f9 Use SHA256 For Query Result Computation 2025-05-01 03:56:20 -04:00
Yak! b5cfa2b992 fix: Multiple actions in an action module do not work as intended. 2025-05-01 16:41:06 +09:00
dependabot[bot] c826717343 build(deps): bump @tiptap/core from 2.10.0 to 2.11.9
Bumps [@tiptap/core](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/core) from 2.10.0 to 2.11.9.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/core@2.11.9/packages/core/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/core@2.11.9/packages/core)

---
updated-dependencies:
- dependency-name: "@tiptap/core"
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 05:44:35 +00:00
Tim Jaeryang Baek 35a64b55a1 Merge pull request #13386 from open-webui/dependabot/npm_and_yarn/dev/tiptap/extension-code-block-lowlight-2.11.9
build(deps): bump @tiptap/extension-code-block-lowlight from 2.10.0 to 2.11.9
2025-04-30 22:43:30 -07:00
Tim Jaeryang Baek 9695e08411 Merge pull request #13392 from open-webui/dependabot/pip/backend/dev/pytest-approx-eq-8.3.5
build(deps): update pytest requirement from ~=8.3.2 to ~=8.3.5 in /backend
2025-04-30 22:43:09 -07:00
Tim Jaeryang Baek c6d76acf76 Merge pull request #13393 from open-webui/dependabot/pip/backend/dev/elasticsearch-9.0.1
build(deps): bump elasticsearch from 8.17.1 to 9.0.1 in /backend
2025-04-30 22:42:55 -07:00
Tim Jaeryang Baek b6a76dc026 Merge pull request #13394 from open-webui/dependabot/pip/backend/dev/loguru-0.7.3
build(deps): bump loguru from 0.7.2 to 0.7.3 in /backend
2025-04-30 22:42:45 -07:00
Timothy Jaeryang Baek 079af5fffe fix: send webhook notification when user is not active 2025-05-01 09:41:07 +04:00
Tim Jaeryang Baek e57f2c928a Merge pull request #13379 from NoMoreFood/dev
feat: Azure STT Allow Base URL & Max Speaker Setting
2025-04-30 22:26:18 -07:00
hurxxxx 513d5c8bf5 Fix typos 2025-05-01 13:21:15 +09:00
hurxxxx d72e41c563 Consolidate duplicate messages into a single one 2025-05-01 13:16:47 +09:00
hurxxxx 1ca5d532b8 chore(i18n): update parsed i18n data and Korean translations 2025-05-01 13:00:27 +09:00
hurxxxx f46c7d53b7 update Korean translation 2025-05-01 12:55:07 +09:00
hurxxxx a7cbf62c68 update Korean translation 2025-05-01 12:43:19 +09:00
dependabot[bot] 4eaaeb1888 build(deps): bump loguru from 0.7.2 to 0.7.3 in /backend
Bumps [loguru](https://github.com/Delgan/loguru) from 0.7.2 to 0.7.3.
- [Release notes](https://github.com/Delgan/loguru/releases)
- [Changelog](https://github.com/Delgan/loguru/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/Delgan/loguru/compare/0.7.2...0.7.3)

---
updated-dependencies:
- dependency-name: loguru
  dependency-version: 0.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:59 +00:00
dependabot[bot] 753d530cd0 build(deps): bump elasticsearch from 8.17.1 to 9.0.1 in /backend
Bumps [elasticsearch](https://github.com/elastic/elasticsearch-py) from 8.17.1 to 9.0.1.
- [Release notes](https://github.com/elastic/elasticsearch-py/releases)
- [Commits](https://github.com/elastic/elasticsearch-py/compare/v8.17.1...v9.0.1)

---
updated-dependencies:
- dependency-name: elasticsearch
  dependency-version: 9.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:56 +00:00
dependabot[bot] 10f55d39d2 build(deps): update pytest requirement in /backend
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.3.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:52 +00:00
dependabot[bot] 0b84863d76 build(deps): bump rapidocr-onnxruntime from 1.3.24 to 1.4.4 in /backend
Bumps [rapidocr-onnxruntime](https://github.com/RapidAI/RapidOCR) from 1.3.24 to 1.4.4.
- [Release notes](https://github.com/RapidAI/RapidOCR/releases)
- [Commits](https://github.com/RapidAI/RapidOCR/compare/v1.3.24...v1.4.4)

---
updated-dependencies:
- dependency-name: rapidocr-onnxruntime
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:49 +00:00
dependabot[bot] efc163fede build(deps): bump codemirror-lang-hcl from 0.0.0-beta.2 to 0.1.0
Bumps codemirror-lang-hcl from 0.0.0-beta.2 to 0.1.0.

---
updated-dependencies:
- dependency-name: codemirror-lang-hcl
  dependency-version: 0.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:53:31 +00:00
dependabot[bot] dc5107a473 build(deps): bump prosemirror-schema-list from 1.4.1 to 1.5.1
Bumps [prosemirror-schema-list](https://github.com/prosemirror/prosemirror-schema-list) from 1.4.1 to 1.5.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-schema-list/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-schema-list/compare/1.4.1...1.5.1)

---
updated-dependencies:
- dependency-name: prosemirror-schema-list
  dependency-version: 1.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:53:26 +00:00
dependabot[bot] c5fd40d951 build(deps): bump @tiptap/extension-code-block-lowlight
Bumps [@tiptap/extension-code-block-lowlight](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-code-block-lowlight) from 2.10.0 to 2.11.9.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/extension-code-block-lowlight@2.11.9/packages/extension-code-block-lowlight/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/extension-code-block-lowlight@2.11.9/packages/extension-code-block-lowlight)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-code-block-lowlight"
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:53:05 +00:00
dependabot[bot] 317032aa5c build(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 8.31.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.21.0 to 8.31.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.31.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.31.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:52:50 +00:00
Tim Jaeryang Baek f3b38e9777 Merge pull request #13372 from therealmichaelberna/main
**fix** convert_function_to_pydantic_model returning empty descriptions
2025-04-30 13:36:22 -07:00
Tim Jaeryang Baek 336d20fa33 Merge pull request #13364 from NovoNordisk-OpenSource/websocket-behind-auth
refactor: hiding websocket initalization behind authentication
2025-04-30 13:35:27 -07:00
Timothy Jaeryang Baek dadfa5f043 refac: HTML_FILE_ID handling 2025-05-01 00:32:29 +04:00
Timothy Jaeryang Baek e52e45d613 refac: collapsible 2025-04-30 22:26:07 +04:00
Michael Berna 4a6b964a51 Fixed convert_function_to_pydantic_model
convert_function_to_pydantic_model was returning empty function descriptions. I have renamed the variables more precisely which helps to avoid confusion and also resolved this issue which was caused by a variable naming conflict.
2025-04-30 14:22:38 -04:00
Timothy Jaeryang Baek 41904f21e4 refac 2025-04-30 19:41:16 +04:00
Timothy Jaeryang Baek ef2aeb7c0e fix: only allow admin uploaded html to be rendered as html 2025-04-30 19:34:53 +04:00
shcodenick 4c81b6ebc5 hiding websocket initalization behind authentication 2025-04-30 15:05:57 +02:00
Timothy Jaeryang Baek 783d409b1d refac 2025-04-30 16:58:38 +04:00
Timothy Jaeryang Baek c1fa989cb8 refac: styling 2025-04-30 16:54:51 +04:00
Bryan Berns 6c8a9d000e Azure STT Allow Base URL & Max Speaker Setting 2025-04-30 08:51:01 -04:00
Timothy Jaeryang Baek f9d238e850 refac: user list sub-standard code 2025-04-30 16:49:41 +04:00
Tim Jaeryang Baek db06a925fe Merge pull request #13267 from AnapatChaiwongse/dev
feat: implement pagination for /admin/users
2025-04-30 05:04:14 -07:00
Timothy Jaeryang Baek eb68b21e0f refac 2025-04-30 16:01:23 +04:00
Tim Jaeryang Baek e822984162 Merge pull request #13358 from feddersen-group/fix/load_ext_dependencies_on_startup
fix: install external requirements of tools and functions to prevent deactivation
2025-04-30 04:59:56 -07:00
Alexander Grimm e80ed32aeb fix: install external requirements of admin functions and tools on startups 2025-04-30 11:14:38 +00:00
Tim Jaeryang Baek c9d3bb35c3 Merge pull request #13342 from feddersen-group/feat/embedding_dim
feat: allow pgvector to use large embedding models
2025-04-30 02:41:26 -07:00
Alexander Grimm da9966aca1 ~ truncate vectors for pgvector if too big 2025-04-30 05:35:17 +00:00
Tim Jaeryang Baek 4ee5dd58b7 Merge pull request #13177 from tth37/fix_firecrawl_loader_default_mode
fix: FireCrawlLoader default mode to scrape
2025-04-29 08:39:06 -07:00
Tim Jaeryang Baek e87f2669fa Merge pull request #13191 from tth37/feat_firecrawl_search_engine
feat: Add Firecrawl search engine
2025-04-29 08:38:28 -07:00
Tim Jaeryang Baek 390263742c Merge pull request #13273 from spammenotinoz/dev
**FIX** Update Knowledge ReIndexing to continue upon missing\corrupt collections
2025-04-29 08:37:55 -07:00
Tim Jaeryang Baek 8f4be09dd2 Merge pull request #13315 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-04-29 08:35:28 -07:00
Tim Jaeryang Baek 7b863465a9 Merge pull request #13311 from stephen304/yacy-support
feat: Yacy search support
2025-04-29 08:35:10 -07:00
Timothy Jaeryang Baek 6ae2d2c4ea refac: gpt-image-1 support 2025-04-29 19:34:00 +04:00
Timothy Jaeryang Baek addf44b21f refac 2025-04-29 19:27:54 +04:00
D 8f31b6d74a Fix: Use dynamic Python command to run uvicorn and support pyenv setups.
Updated the uvicorn startup command to dynamically detect and use either 'python3' or 'python',
ensuring compatibility across different environments and pyenv setups.
This avoids issues where only one of the commands is available.
2025-04-29 09:14:23 +01:00
Aleix Dorca bc88a9bc8b Update Catalan translation.json 2025-04-29 07:04:16 +02:00
Tim Jaeryang Baek f7f3ae7cc9 Merge pull request #13306 from tarmst/remove-blocking-websocket-awaits
Perf: Chat Websocket Sequential Awaits
2025-04-28 11:17:50 -07:00
Tim Jaeryang Baek 5a1c4dbe1b Merge pull request #13279 from panda44312/patch-14
i18n: zh-cn
2025-04-28 11:17:24 -07:00
Panda 63e4d1ea41 i18n: fix 2025-04-28 18:22:36 +02:00
tth37 cf2d7de873 refac: Implicit asyncio create_task 2025-04-29 00:20:55 +08:00
tarmst c9a9e05bb4 Fix websocket sequential awaits 2025-04-28 15:17:34 +00:00
Timothy Jaeryang Baek 3ac74a4fcb refac: styling 2025-04-28 18:21:48 +04:00
Timothy Jaeryang Baek 63fe426a52 chore: format 2025-04-28 17:46:05 +04:00
Timothy Jaeryang Baek ee8de1ee0b refac: disable jwt display for non-admin users 2025-04-28 17:45:17 +04:00
Timothy Jaeryang Baek 791c19acf7 enh: unicode char support for user import 2025-04-28 17:26:19 +04:00
Tim Jaeryang Baek a9999833d2 Merge pull request #13185 from Chayud-Ac/feature-thai-i18n
fix(i18n): improve Thai translations
2025-04-28 06:16:20 -07:00
Tim Jaeryang Baek c3da9f3026 Merge pull request #13246 from jmtatsch/fix-video-treated-as-text
Fix uploading video files
2025-04-28 06:15:40 -07:00
Tim Jaeryang Baek 2f5abf7374 Merge pull request #13187 from Mister-Hope/patch-1
feat: update default CUDA version to 12.8, close #13186
2025-04-28 06:15:01 -07:00
Tim Jaeryang Baek dedc45183f Merge pull request #13258 from choket/dev
Fix: Fixed a bug that caused user notification webhooks to never be sent
2025-04-28 06:14:24 -07:00
Tim Jaeryang Baek 6d59a5f809 Merge pull request #13253 from noexcs/dev
fix: The value of the 'Access-Control-Allow-Origin' header in the res…
2025-04-28 06:12:53 -07:00
Timothy Jaeryang Baek 10855cf5ed refac 2025-04-28 17:11:28 +04:00
Timothy Jaeryang Baek 9398c843d3 refac 2025-04-28 17:08:07 +04:00
Timothy Jaeryang Baek 45ad6776de fix: token cookie issue 2025-04-28 17:04:13 +04:00
Tim Jaeryang Baek fb1c101dc6 Merge pull request #13269 from jarrod-lowe/s3-tagging
feat: Add tags to S3 uploaded files
2025-04-28 05:57:08 -07:00
Tim Jaeryang Baek a0273aaea8 Merge pull request #13271 from schulti/patch-1
Update de-DE translation.json
2025-04-28 05:56:22 -07:00
Tim Jaeryang Baek 146a08c68f Merge branch 'dev' into patch-1 2025-04-28 05:49:50 -07:00
Timothy Jaeryang Baek 7d0a78a43a refac: aiohttp trust_env=True 2025-04-28 16:47:34 +04:00
Timothy Jaeryang Baek 9bfb4ae838 fix: ollama proxy "format" 2025-04-28 16:45:23 +04:00
Panda ec496de593 i18n: zh-cn 2025-04-28 11:05:06 +02:00
spammenotinoz 12fd7c8cb9 Update knowledge.py
Improve Error Handling ReIndexing Knowledge Database
2025-04-28 16:39:20 +10:00
Stefan Schultheis, OE1SCS d9186e138b Update de-DE translation.json
added a few translations and corrected some typos
2025-04-28 08:21:21 +02:00
Jarrod Lowe 141810039f Add tags to S3 uploaded files 2025-04-28 16:56:56 +12:00
KaeOhhhh 2112067054 Support pagination in /admin/users API
Update Users.svelte

Update index.ts

Update users.py

Update users.py

Update UserList.svelte
2025-04-28 11:29:42 +07:00
root 8a9de0a62f Fix bug that never triggers user notification webhooks 2025-04-27 21:31:34 +02:00
noexcs 4b9b24ae78 fix: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. 2025-04-27 21:29:13 +08:00
jmtatsch 056a042ef2 Update files.py
fix formatting
2025-04-27 11:18:27 +02:00
jmtatsch c51a91583e Dont attempt to infer charset for video files 2025-04-27 10:59:35 +02:00
nathaniel 1efa708f83 Added WHISPER_LANGUAGE env variable. If set to a country's ISO2, constrains Whisper's stt to that language. Detects language as normal if unset 2025-04-27 05:58:06 +01:00
Stephen Smith ea16426a8d Remove unused kwargs in yacy, update comments. 2025-04-27 00:41:46 -04:00
Stephen Smith f9b9217e98 Set Yacy search to text 2025-04-26 23:13:31 -04:00
Stephen Smith e6d43d70f3 Don't request nav and pass count to Yacy 2025-04-26 23:08:16 -04:00
Stephen Smith 240d91d38d Add yacy config for user/pass, automatically add yacy json api path 2025-04-26 22:28:30 -04:00
Stephen Smith 0f73b96616 first pass at yacy support copied from searxng 2025-04-26 14:07:13 -04:00
Timothy Jaeryang Baek 1349bc47b1 fix: chat message input ui styling 2025-04-25 14:21:58 +09:00
tth37 92dbeb1939 feat: Add Firecrawl search engine 2025-04-24 14:57:28 +08:00
Mister-Hope 5f69424215 Update Dockerfile 2025-04-24 13:59:57 +08:00
Chayud 86b9f252c6 fix(i18n): improve Thai translations 2025-04-24 10:35:02 +07:00
Timothy Jaeryang Baek 0b5514f376 refac 2025-04-24 03:16:46 +09:00
Timothy Jaeryang Baek 28ec3069de enh: exp token check 2025-04-24 03:12:22 +09:00
tth37 8f7195ceda fix: FireCrawlLoader default mode to scrape 2025-04-24 01:17:35 +08:00
Tim Jaeryang Baek 91e758f3ec Merge pull request #13165 from feddersen-group/perf/parallel_knowledge_search
perf: all knowledge searches in parallel in non-hybrid mode
2025-04-23 10:01:06 -07:00
Tim Jaeryang Baek d0a1a0f16b Merge pull request #13119 from jarrod-lowe/suggestions
feat: Set default prompt suggestions from environment variable DEFAULT_PROMPT_SUGGESTIONS
2025-04-23 09:55:57 -07:00
Timothy Jaeryang BaekandDrZoidberg09 732d7aee70 enh: sentence transformers env vars
Co-Authored-By: DrZoidberg09 <96449693+drzoidberg09@users.noreply.github.com>
2025-04-24 01:55:18 +09:00
Timothy Jaeryang Baek 3ec6652f99 chore: dep bump 2025-04-24 01:48:07 +09:00
Tim Jaeryang Baek cad9b292bf Merge pull request #13175 from PeterDaveHello/Improve-zh-TW-locale
i18n: improve zh-TW Traditional Chinese locale
2025-04-23 09:41:23 -07:00
Timothy Jaeryang Baek 09874ab83d fix: FireCrawlLoader 2025-04-24 01:40:34 +09:00
Peter Dave Hello c798adb04c i18n: improve zh-TW Traditional Chinese locale 2025-04-24 00:37:15 +08:00
Timothy Jaeryang Baek 7baca2b483 fix: ollama url model download issue 2025-04-24 01:26:19 +09:00
Tim Jaeryang Baek 2b10b378b6 Merge pull request #13170 from TiancongLx/dev
i18n: update zh-TW
2025-04-23 08:22:27 -07:00
Tim Jaeryang Baek 2652b91f5a Merge pull request #13166 from oleguldberg/update-danish-translation
i18n - Add to Danish Translation
2025-04-23 08:22:17 -07:00
Tiancong Li afdba443f4 i18n: update zh-TW 2025-04-23 21:28:46 +08:00
Ole Guldberg ba4f399ace Merge branch 'update-danish-translation' of https://github.com/oleguldberg/open-webui into update-danish-translation 2025-04-23 14:51:01 +02:00
Ole Guldberg 176ff56e85 Merge branch 'update-danish-translation' of https://github.com/oleguldberg/open-webui into update-danish-translation 2025-04-23 12:30:19 +02:00
Ole Guldberg f2765bee72 More Danish Translation
i18n - add to Danish translation. Made agains -dev.
2025-04-23 12:25:32 +02:00
Ole Guldberg 497a7b4ded More Danish Translation
i18n - add to Danish translation. Made agains -dev.
2025-04-23 12:06:17 +02:00
Alexander Grimm d182155fac ~ call knowledge searches in parallel in non-hybrid mode 2025-04-23 09:20:51 +00:00
Timothy Jaeryang Baek 5030041683 chore: format 2025-04-23 16:05:15 +09:00
Tim Jaeryang Baek faa3cac0e4 Merge pull request #13107 from tth37/fix_tavily_max_results
fix: `max_results` in Tavily search handler
2025-04-22 23:47:36 -07:00
Tim Jaeryang Baek d3e516934c Merge pull request #13098 from athoik/dev
feat: Add abstract base class for vector database integration
2025-04-22 23:24:08 -07:00
Timothy Jaeryang Baek 2f7b5acdf8 enh: chat share & export permissions 2025-04-23 14:43:33 +09:00
Tim Jaeryang Baek 7937ed3ee1 Merge pull request #13144 from PeterDaveHello/FixI18nInMultiResponseMessages
i18n: Add missing i18n locale for "Merged Response" string
2025-04-22 20:38:37 -07:00
Tim Jaeryang Baek f0a129482e Merge pull request #13145 from PeterDaveHello/Update-zh-TW-locale
i18n: improve zh-TW Traditional Chinese locale
2025-04-22 20:37:47 -07:00
Ole Guldberg 0e0ee3ca9c More Danish Translation 2025-04-22 21:32:21 +02:00
Ole Guldberg 26a2187e6e More Danish translation 2025-04-22 21:30:05 +02:00
Ole Guldberg 2b4e946f70 More danish 2025-04-22 21:10:51 +02:00
Ole Guldberg 17a9d11435 More dansh 2025-04-22 21:00:36 +02:00
Ole Guldberg a21ac3cc25 More danish 2025-04-22 20:54:33 +02:00
Peter Dave Hello 4eb6985c1b i18n: improve zh-TW Traditional Chinese locale 2025-04-23 02:03:07 +08:00
Peter Dave Hello 5f81755b86 i18n: Add missing i18n locale for "Merged Response" string 2025-04-23 02:01:18 +08:00
Ole Guldberg 4340431753 More danish translation 2025-04-22 18:10:43 +02:00
Timothy Jaeryang Baek f2314596ba enh: failed login attempts audit log 2025-04-23 00:06:44 +09:00
Timothy Jaeryang Baek 43efff0fe6 refac 2025-04-22 23:22:50 +09:00
Ole Guldberg 4e8cd688bf beginning of update 2025-04-22 16:05:13 +02:00
Jarrod Lowe af5efe1580 Set default prompt suggestions from environment variable 2025-04-22 11:39:01 +12:00
tth37 5b9c1de3d1 fix: Translatable 'Searching the web' 2025-04-22 01:24:30 +08:00
tth37 4b451b984b perf: Multi-thread web searching 2025-04-21 22:18:05 +08:00
tth37 bc315bd530 fix: max_results in Tavily search api 2025-04-21 20:59:47 +08:00
Athanasios Oikonomou 1e291aff25 feat: Add abstract base class for vector database integration
- Created `VectorDBBase` as an abstract base class to standardize vector database operations.
- Added required methods for common vector database operations: `has_collection`, `delete_collection`, `insert`, `upsert`, `search`, `query`, `get`, `delete`, `reset`.
- The base class can now be extended by any vector database implementation (e.g., Qdrant, Pinecone) to ensure a consistent API across different database systems.
2025-04-21 08:27:27 +03:00
Tim Jaeryang Baek 913f8a15f9 Merge pull request #13073 from athoik/dev
feat(vector-db): add support for Pinecone client
2025-04-20 14:42:36 -07:00
Tim Jaeryang Baek 7f85436a94 Merge pull request #13089 from izumi0004/fix/tool_calls
fix: handling tool calls in streamed responses
2025-04-20 14:08:09 -07:00
izumi0004 99ce85f20f fix: handling tool calls in streamed responses 2025-04-21 00:29:52 +08:00
ayan4m1 039dec6820 fix: pass header to Tika if PDF_EXTRACT_IMAGES is true 2025-04-20 17:36:40 +02:00
Athanasios Oikonomou e000c56ef7 feat(vector-db): add support for Pinecone client
Adds Pinecone as a supported vector database option.

- Implements `PineconeClient` with support for common operations: `add`, `query`, `delete`, `reset`.
- Emulates namespace support using metadata filtering (`collection_name` prefix).
- Dynamically configures Pinecone via the following env vars:
  - `PINECONE_API_KEY`
  - `PINECONE_ENVIRONMENT`
  - `PINECONE_INDEX_NAME`
  - `PINECONE_DIMENSION`
  - `PINECONE_METRIC`
  - `PINECONE_CLOUD`
- Integrates cleanly with the vector DB abstraction layer.
- Includes markdown documentation under `docs/getting-started/env-configuration.md`.

BREAKING CHANGE: None
2025-04-20 11:08:51 +03:00
Tim Jaeryang Baek af02708b4c Merge pull request #13072 from grandtele/fix/openai_o_series_handler
* fix: Refactor openai handler to support all o-series models universally
2025-04-20 00:42:39 -07:00
grand 5a8e417590 * refactor openai handler to support all o-series models universally 2025-04-20 09:14:27 +02:00
Timothy Jaeryang Baek 1a6e4c2948 refac 2025-04-19 23:40:31 -07:00
Timothy Jaeryang Baek 6fd082d55f enh: html token (allow yt embed) 2025-04-19 23:38:08 -07:00
Timothy Jaeryang Baek f4de3b4c45 enh: notificationSoundAlways option 2025-04-19 23:13:19 -07:00
Tim Jaeryang Baek 8fcad1df4e Merge pull request #13062 from open-webui/cla
chore: cla
2025-04-19 11:33:02 -07:00
Tim Jaeryang Baek 08a35417f2 Merge branch 'dev' into cla 2025-04-19 11:32:49 -07:00
Timothy Jaeryang Baek f7906790e1 refac 2025-04-19 11:32:16 -07:00
Timothy Jaeryang Baek 852d9dcbe9 chore: cla 2025-04-19 11:30:59 -07:00
Timothy Jaeryang Baek 48637893b1 fix: scroll on branch change w/ multi response chat 2025-04-19 04:09:46 -07:00
Timothy Jaeryang Baek 463d7fb628 refac: openapi to tool spec 2025-04-19 03:46:06 -07:00
Timothy Jaeryang Baek 332f8579d7 doc: readme 2025-04-19 02:55:02 -07:00
Timothy Jaeryang Baek ad8c8da438 refac 2025-04-19 02:47:48 -07:00
Timothy Jaeryang Baek f0447b24ab refac: wording 2025-04-19 01:51:40 -07:00
Timothy Jaeryang Baek 63a887151f chore: pr template 2025-04-19 00:47:52 -07:00
Timothy Jaeryang Baek 0aea894e2d Create CONTRIBUTOR_LICENSE_AGREEMENT.md 2025-04-19 00:41:08 -07:00
Timothy Jaeryang Baek 8507c47459 refac: WEBUI_AUTH_SIGNOUT_REDIRECT_URL 2025-04-18 23:58:37 -07:00
Tim Jaeryang Baek 221303eec5 Merge pull request #12592 from jarrod-lowe/main
feat: Redirect on sign out
2025-04-18 23:56:38 -07:00
Tim Jaeryang Baek 87844a8042 Merge pull request #12822 from tth37/feat_external_search_loader
feat: Support for Self-Hosted/External Web Search/Loader Engines
2025-04-18 23:51:27 -07:00
Timothy Jaeryang Baek 1c18282d19 fix 2025-04-18 18:21:49 -07:00
Tim Jaeryang Baek 6101067ea4 Merge pull request #12974 from holmlund/fix/prompts-international
fix: improve international character handling in prompt commands
2025-04-18 18:19:12 -07:00
Tim Jaeryang Baek d2f3c82fd6 Merge pull request #13031 from gaby/anyio-workers
feat: Add support for configuring FastAPI/AnyIO Thread Pool
2025-04-18 17:53:35 -07:00
Tim Jaeryang Baek 27936014b0 Merge pull request #13039 from taylorwilsdon/jit_oauth_groups
feat: Just-in-Time Group Creation for OAuth Groups
2025-04-18 17:53:04 -07:00
Timothy Jaeryang Baek 8b9527d887 refac 2025-04-18 17:14:09 -07:00
Timothy Jaeryang Baek a5476ef7d8 refac 2025-04-18 17:10:44 -07:00
Juan Calderon-Perez 62f87c2e38 Use default AnyIO pool size 2025-04-18 19:21:54 -04:00
Timothy Jaeryang Baek 60d84a3aae chore: license "branding" clause 2025-04-18 14:49:20 -07:00
Tim Jaeryang Baek 3e7bce0921 Merge pull request #13036 from erandow/translate-persian-texts
feat: translate all texts to persian
2025-04-18 12:32:17 -07:00
Taylor Wilsdon 1dbf4d0461 Add ENABLE_OAUTH_GROUP_CREATION flag for JIT group creation with OAuth group updates enabled 2025-04-18 10:17:08 -07:00
Taylor Wilsdon a67e5f725f Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-18 10:04:42 -07:00
Erfan Asadi 527c5e73dc fix: revert اولاما to ollama after feedback 2025-04-18 18:56:01 +03:30
Erfan Asadi 2ecfd3dbed Merge branch 'dev' of https://github.com/erandow/open-webui into translate-persian-texts 2025-04-18 18:55:14 +03:30
Erfan Asadi 5989b6ca12 Merge branch 'main' of https://github.com/erandow/open-webui into translate-persian-texts 2025-04-18 18:54:35 +03:30
Erfan Asadi e3af831db1 feat: translate the remaining texts in persian 2025-04-18 18:38:06 +03:30
Juan Calderon-Perez f77f6cd1e2 Update imports to match anyio docs 2025-04-18 09:30:34 -04:00
Juan Calderon-Perez e19554e9a8 Simplify import 2025-04-18 09:26:57 -04:00
Juan Calderon-Perez 19b3d88737 Add support for configuring FastAPI/AnyIO Thread Pool Size 2025-04-18 09:22:23 -04:00
Timothy Jaeryang Baek 0d09e46281 refac 2025-04-18 05:32:11 -07:00
Timothy Jaeryang Baek 961869a60c refac 2025-04-18 05:30:50 -07:00
Timothy Jaeryang Baek b8fb4e528d enh: toast close button 2025-04-18 04:33:18 -07:00
Tim Jaeryang Baek 794360addc Merge pull request #12997 from Elmolesto/feat/knowledge-lazy-load-content
feat: Lazy load file content on click
2025-04-18 04:04:29 -07:00
Tim Jaeryang Baek fa8132903e Merge pull request #12904 from hurxxxx/feat/onedrive-orgs
feat: support Organization and School accounts in OneDrive
2025-04-18 04:03:54 -07:00
Timothy Jaeryang Baek a86a932b46 refac: deprecate event dispatcher 2025-04-18 02:38:53 -07:00
Felipe Hernandez 14fa7d16f9 feat: Cache file content 2025-04-18 11:38:15 +02:00
Timothy Jaeryang Baek 64a98b2e0e refac: manifest orientation "any" 2025-04-18 02:24:44 -07:00
Timothy Jaeryang Baek 8f67ffabbd fix: model editor system not being preserved 2025-04-18 02:23:25 -07:00
Timothy Jaeryang Baek a22be555cb refac 2025-04-18 01:26:42 -07:00
Timothy Jaeryang Baek eb80719cf0 feat: AIOHTTP_CLIENT_SESSION_SSL 2025-04-17 22:11:42 -07:00
Tim Jaeryang Baek 840dd9c89a Merge pull request #12971 from tth37/fix_source_indexing
fix: Align backend <source> tag indexing with frontend citation grouping
2025-04-17 22:08:07 -07:00
Tim Jaeryang Baek 30e84adb51 Merge pull request #12883 from Classic298/patch-1
fix: UVICORN_WORKERS __init__.py pass application as string
2025-04-17 22:07:21 -07:00
Tim Jaeryang Baek 414ebc87ee Merge pull request #12894 from Classic298/patch-2
security/fix: prevent email and password changes to the primary admin account
2025-04-17 22:06:54 -07:00
Tim Jaeryang Baek 165eb50c22 Merge pull request #12958 from Youggls/fix_search_web
fix: use run_in_threadpool for search_web to prevent blocking
2025-04-17 22:03:31 -07:00
Tim Jaeryang Baek 477ac7de86 Merge pull request #12968 from alpha-pet/feat-openapi-parameter-description
feat: support OpenAPI parameter description and enum values in tool spec
2025-04-17 22:02:50 -07:00
Timothy Jaeryang Baek 824238a620 enh: AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL 2025-04-17 22:02:16 -07:00
Felipe Hernandez 88f97372d9 fix: Show content on upload file 2025-04-18 02:01:47 +02:00
Felipe Hernandez 0577dc2990 feat: Lazy load file content on click 2025-04-18 01:40:30 +02:00
Tim Jaeryang Baek 3b20c57ea8 Merge pull request #12956 from lukasz-pekala/feat/improve-polish-translation
fix: update Polish translations for clarity and consistency
2025-04-17 12:38:14 -07:00
Tim Jaeryang Baek 43b9bf17dc Merge pull request #12966 from damianmr/dev
i18n: Update es-ES (Spanish) translation.json
2025-04-17 12:38:03 -07:00
Tim Jaeryang Baek 246ff83864 Merge pull request #12981 from knight-upstage/dev
i18n: Update ko-KR (Korean) translation.json
2025-04-17 12:37:41 -07:00
knight-upstage cdf26d8189 i18n: Update ko-KR (Korean) translation.json 2025-04-17 09:57:22 -07:00
tth37 79bde6fa47 fix: Align backend <source> tag indexing with frontend citation grouping 2025-04-17 22:27:22 +08:00
Thomas Rehn 3dcbf1acf5 feat: support OpenAPI parameter description and enum values in tool spec 2025-04-17 15:48:55 +02:00
Damian Mendez Romera b1629d8660 i18n: Update es-ES (Spanish) translation.json 2025-04-17 13:58:00 +01:00
David Holmlund 9f8b94e45f fix: improve international character handling in prompt commands 2025-04-17 14:56:57 +02:00
Youggls 9669cd3454 fix: use run_in_threadpool for search_web to prevent blocking
Used fastapi's run_in_threadpool function to execute the search_web function,
preventing the synchronous function from blocking the entire web search process.
2025-04-17 17:23:20 +08:00
Łukasz Pękala c4edcdb12a fix: update Polish translations for clarity and consistency 2025-04-17 10:56:23 +02:00
Timothy Jaeryang Baek a6ccc48d91 refac: textarea component 2025-04-17 00:35:39 -07:00
Tim Jaeryang Baek b6caa95fac Merge pull request #12947 from gaby/qdrant-grpc
feat: Add gRPC support to Qdrant
2025-04-16 23:08:50 -07:00
Juan Calderon-Perez 6188c0c5b7 Add suport for Qdrant GRPC 2025-04-17 01:13:49 -04:00
Jarrod Lowe dfe50b4e56 Merge branch 'open-webui:main' into main 2025-04-17 15:46:24 +12:00
Taylor Wilsdon 54c6e64eec Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-16 13:01:04 -07:00
Tim Jaeryang Baek 465e879a78 Merge pull request #12917 from rimvel/patch-1
Update translation.json
2025-04-16 08:05:44 -07:00
Tim Jaeryang Baek 93fd5185f9 Merge pull request #12921 from aleixdorca/dev
i18n: Update catalan translation.json
2025-04-16 08:05:14 -07:00
Tim Jaeryang Baek 4ebee592f2 Merge pull request #12925 from JaworIwanowRoosi/main
fix: typos in German translation
2025-04-16 08:05:05 -07:00
JaworIwanowRoosi 6076476a74 fixed: typos in German translation 2025-04-16 14:47:15 +02:00
Aleix Dorca 59918b0f54 Update catalan translation.json 2025-04-16 11:20:03 +02:00
Rimvydas bb1dea72d4 Update translation.json
Fixed typo from "Tęstti" to "Tęsti" (continue).
2025-04-16 09:59:20 +03:00
Tim Jaeryang Baek 264a87f467 Merge pull request #12901 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-04-15 19:03:52 -07:00
Tim Jaeryang Baek 07347f518a Merge pull request #12896 from gaby/fix-12893
fix: Optional content from files routes
2025-04-15 15:54:29 -07:00
hurxxxx 84f5a52923 chore: clean up unnecessary code 2025-04-16 02:38:20 +09:00
Kylapaallikko 8a50d50038 Update translation.json 2025-04-15 19:44:20 +03:00
hurxxxx 41ef596472 Merge remote-tracking branch 'upstream/main' into feat/onedrive-orgs 2025-04-16 01:41:24 +09:00
Juan Calderon-Perez b4d0d840d1 Fix formatting of qdrant.py 2025-04-15 08:56:51 -04:00
Juan Calderon-Perez b1ef53873f Check if content is present before removing 2025-04-15 08:53:05 -04:00
Classic298 4f14b17c34 Update users.py 2025-04-15 13:50:52 +02:00
Classic298 36b2052fb0 Update __init__.py 2025-04-15 09:55:35 +02:00
Timothy Jaeryang Baek 12c7ecf0f4 refac: styling 2025-04-14 22:34:38 -07:00
Tim Jaeryang Baek e5cbef0baa Merge pull request #12868 from athoik/dev
feat: add QDRANT_ON_DISK configuration option for Qdrant integration
2025-04-14 21:31:46 -07:00
Tim Jaeryang Baek 44e2785495 Merge pull request #12859 from aindriu80/main
i18n: Updated Irish translation
2025-04-14 21:30:21 -07:00
Tim Jaeryang Baek f96c610e21 Merge pull request #12847 from panda44312/patch-13
i18n: zh-cn fix
2025-04-14 21:30:17 -07:00
Taylor Wilsdon 0fc5699533 Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-14 19:34:12 -04:00
Athanasios Oikonomou 575c12f80c feat: add QDRANT_ON_DISK configuration option for Qdrant integration
This commit will allow configuring the on_disk client parameter, to reduce the memory usage.
https://qdrant.tech/documentation/concepts/storage/?q=mmap#configuring-memmap-storage
Default is false, keeping vectors in memory.
2025-04-15 01:40:57 +03:00
Tim Jaeryang Baek 7e0e6177e9 Merge pull request #12865 from TiancongLx/dev
i18n: update zh-TW
2025-04-14 13:53:24 -07:00
Tiancong Li 0d388b4e54 i18n: update zh-TW 2025-04-15 03:33:30 +08:00
Aindriú Mac Giolla Eoin a3d950872c Updated Irish translations - April 2025-04-14 17:14:02 +01:00
hurxxxx 55d077b52a refactor 2025-04-15 00:35:18 +09:00
hurxxxx 2d7062fc99 fix: onedrive orgs selection 2025-04-15 00:27:59 +09:00
hurxxxx 5fd794612e add onedrive sub menu 2025-04-14 22:57:32 +09:00
Panda 21206bf4b9 i18n: zh-cn fix 2025-04-14 14:26:10 +02:00
tth37 dcb3f18e1e refac: styling 2025-04-14 18:56:47 +08:00
tth37 85f8e91288 feat: Allow admin editing external search/loader settings 2025-04-14 18:19:26 +08:00
tth37 008fec80c1 fix: Update external search/loader method to POST 2025-04-14 18:17:27 +08:00
Tim Jaeryang Baek 07d8460126 Merge pull request #12809 from open-webui/dev
0.6.5
2025-04-14 02:13:21 -07:00
Timothy Jaeryang Baek e4c7417522 refac 2025-04-14 02:12:58 -07:00
Timothy Jaeryang Baek 91523bbb07 chore: bump 2025-04-14 02:08:11 -07:00
Timothy Jaeryang Baek 49bf3207e8 chore: format 2025-04-14 02:00:52 -07:00
Timothy Jaeryang Baek ec730b08c5 chore: format 2025-04-14 02:00:46 -07:00
Timothy Jaeryang Baek 815d18b57e revert 2025-04-14 01:57:23 -07:00
Timothy Jaeryang Baek 8d33b5e063 refac 2025-04-14 01:56:52 -07:00
Timothy Jaeryang BaekandSebastian Whincop aa8db40376 enh: copy formatted option
Co-Authored-By: Sebastian Whincop <123417897+macjedi42@users.noreply.github.com>
2025-04-14 01:56:15 -07:00
Timothy Jaeryang Baek 95fca7b700 refac 2025-04-14 01:41:58 -07:00
Timothy Jaeryang Baek 87aa01a5c2 refac: styling 2025-04-14 01:40:29 -07:00
Timothy Jaeryang Baek d932fc555d enh: tts/stt user permissions 2025-04-14 01:40:22 -07:00
Timothy Jaeryang Baek ec3b8fab5b refac 2025-04-14 01:08:16 -07:00
Timothy Jaeryang Baek 403295600f refac: audio tts voice list 2025-04-14 00:20:51 -07:00
Tim Jaeryang Baek ceffac87b6 Merge pull request #12835 from weisser-dev/dev
Fix: Robust Handling of LDAP Email Attribute Types
2025-04-14 00:15:51 -07:00
Timothy Jaeryang Baek e7332fd6fe refac 2025-04-13 23:39:38 -07:00
Tim Jaeryang Baek 60596c362a Merge pull request #12815 from TomBayne/enh/vad_filter
feat/Enable vad_filter to improve quality of transcription in faster-whisper
2025-04-13 23:27:03 -07:00
Erik c749083935 Update auths.py 2025-04-14 08:12:37 +02:00
Timothy Jaeryang Baek 70718dda90 refac 2025-04-13 22:31:43 -07:00
Timothy Jaeryang Baek 2fcf5d8578 refac 2025-04-13 17:28:09 -07:00
Tim Jaeryang Baek bc2b5d3cc8 Merge pull request #12816 from SadmL/dev
[i18n] Russian locale update
2025-04-13 14:11:16 -07:00
Tim Jaeryang Baek 370f934158 Merge pull request #12819 from athoik/dev
fix: Properly handle ldap3.abstract.attribute.Attribute value
2025-04-13 14:09:56 -07:00
tth37 22f0365cef format 2025-04-14 02:05:58 +08:00
tth37 839ba22c90 feat: Backend for Self-Hosted/External Web Search/Loader Engines 2025-04-14 01:49:05 +08:00
Athanasios Oikonomou c70fedb626 Properly handle ldap3.abstract.attribute.Attribute value
This commit fixes an issue created on commit 2e7a01f30a.

Ldap object is type <class 'ldap3.abstract.attribute.Attribute'>.
So we need to return the value and verify if it's null, string or list.

Fixes #12813
2025-04-13 18:00:42 +03:00
SadmL 08bcfdb8cc [i18n] Russian locale update 2025-04-13 15:37:18 +03:00
Tom 24367d459b Enable vad_filter to improve quality of transcription in faster-whisper model. 2025-04-13 13:03:57 +01:00
Tim Jaeryang Baek 8158609c93 Merge pull request #12810 from tth37/i18n_small_fix
i18n: update zh-CN
2025-04-12 23:15:54 -07:00
tth37 70d9405e64 i18n: update zh-CN 2025-04-13 14:14:34 +08:00
Timothy Jaeryang Baek aaefba2777 chore: bump 2025-04-12 23:06:40 -07:00
Tim Jaeryang Baek aca37f592d Merge pull request #12807 from open-webui/dev
0.6.4
2025-04-12 23:01:19 -07:00
Timothy Jaeryang Baek 888b468576 fix 2025-04-12 23:00:34 -07:00
Timothy Jaeryang Baek 9348c05f1a doc: changelog 2025-04-12 22:57:13 -07:00
Timothy Jaeryang Baek 4dafbbccfc fix: rag template display issue 2025-04-12 22:55:24 -07:00
Tim Jaeryang Baek 8b0e565e2c Merge pull request #12715 from open-webui/dev
0.6.3
2025-04-12 22:44:24 -07:00
Timothy Jaeryang Baek 2766d7df6a doc: changelog 2025-04-12 22:44:00 -07:00
Tim Jaeryang Baek 8e810e758d Merge pull request #12806 from tth37/fix_web_rag_settings
fix: Resolve small bugs on updated web/rag settings
2025-04-12 22:26:04 -07:00
Timothy Jaeryang Baek e7821288f5 doc: changelog 2025-04-12 22:25:02 -07:00
tth37 db9d4ead59 format 2025-04-13 13:10:39 +08:00
Timothy Jaeryang Baek 1a6eaf6445 chore: dependency bump 2025-04-12 22:07:23 -07:00
tth37 8d53f1e770 fix: small bugs on updated web/rag settings 2025-04-13 12:55:50 +08:00
Timothy Jaeryang Baek c1444cbbba refac 2025-04-12 21:47:46 -07:00
Timothy Jaeryang Baek 40f9747ae4 refac 2025-04-12 21:44:47 -07:00
Timothy Jaeryang Baek ac8e81834c refac 2025-04-12 21:44:15 -07:00
Timothy Jaeryang Baek 8d62cc1935 refac 2025-04-12 21:09:26 -07:00
Timothy Jaeryang Baek f3fe82da80 fix: ongoing chat stop issue 2025-04-12 20:51:02 -07:00
Timothy Jaeryang Baek fa61065c1e refac 2025-04-12 20:15:00 -07:00
Timothy Jaeryang Baek 5844779384 refac: styling 2025-04-12 20:14:32 -07:00
Timothy Jaeryang Baek 451465151e refac 2025-04-12 19:31:02 -07:00
Tim Jaeryang Baek 85ef912f6a Merge pull request #12794 from TiancongLx/dev
i18n: update zh-TW
2025-04-12 19:27:26 -07:00
Tiancong Li 2b8cd4dff4 i18n: update zh-TW 2025-04-13 10:00:58 +08:00
Timothy Jaeryang Baek f6c8184a74 fix: model hide issue 2025-04-12 18:49:49 -07:00
Timothy Jaeryang Baek bde89fd29e refac: audio 2025-04-12 18:40:09 -07:00
Timothy Jaeryang Baek daabc188e8 fix: model list hide issue 2025-04-12 18:34:10 -07:00
Timothy Jaeryang Baek d5ee39de21 fix: katex display overflow issue 2025-04-12 18:27:23 -07:00
Timothy Jaeryang Baek cdb4904999 refac 2025-04-12 18:11:22 -07:00
Tim Jaeryang Baek 577406e00b Merge pull request #12788 from TiancongLx/dev
i18n: update zh-TW
2025-04-12 17:58:01 -07:00
Timothy Jaeryang Baek 175919c787 enh: widescreen mode in shared chats 2025-04-12 17:53:23 -07:00
Tiancong Li 03e2e5279f i18n: update zh-TW 2025-04-13 08:50:22 +08:00
Timothy Jaeryang Baek 4ca6247496 chore: format 2025-04-12 17:45:33 -07:00
Timothy Jaeryang Baek 40a3e7786b refac: voice recording 2025-04-12 17:41:53 -07:00
Timothy Jaeryang Baek 79dff685ea feat: multiple models chat user permissions 2025-04-12 17:37:30 -07:00
Timothy Jaeryang Baek 881d81d87f refac: default tool calling behaviour 2025-04-12 17:26:35 -07:00
Timothy Jaeryang Baek db8780fd9c refac: default tool boilerplate 2025-04-12 17:16:27 -07:00
Timothy Jaeryang Baek 0a69db291d enh: iframe sandbox policy setting 2025-04-12 17:09:39 -07:00
Timothy Jaeryang Baek af0d99b2a3 refac: artifacts styling 2025-04-12 17:01:45 -07:00
Tim Jaeryang Baek 540c27c589 Merge pull request #12784 from rgaricano/dev
UPD: Spanish Translation (es-ES)  Rev.0.6.3
2025-04-12 16:51:47 -07:00
_00_ 4106681889 UPD: Spanish Translation (es-ES) Rev.0.6.3
Revision of Spanish Translation (es-ES)
Adapted to Open-WebUI v.0.6.3

Has been done

Revision & Complete.
2025-04-13 01:42:28 +02:00
Timothy Jaeryang Baek 91a455a284 chore: format 2025-04-12 16:35:11 -07:00
Timothy Jaeryang Baek 77b25ae36a refac 2025-04-12 16:34:20 -07:00
Timothy Jaeryang Baek 48a23ce3fe refac: web/rag config 2025-04-12 16:33:36 -07:00
Timothy Jaeryang Baek c3497da5dd enh: only copy text message content 2025-04-12 15:11:03 -07:00
Timothy Jaeryang Baek bdef1001ac refac 2025-04-12 15:10:43 -07:00
Tim Jaeryang Baek 6862d8fba8 Merge pull request #12769 from tth37/feat_frontend_web_loader
feat: Add Frontend Configuration for `RAG_WEB_LOADER_ENGINE`
2025-04-12 14:56:47 -07:00
Tim Jaeryang Baek 02ba7aeb95 Merge pull request #12366 from Ithanil/redis_sentinel_2
chore: adapt websocket redis sentinel code to new native support in python-socketio
2025-04-12 11:41:30 -07:00
Jan Kessler c6755f9151 bump python-socketio to 5.13.0 (to support Redis Sentinel natively) 2025-04-12 19:22:10 +02:00
tth37 5eac5960ef feat: Add frontend configuration for web loader 2025-04-12 17:13:30 +08:00
Timothy Jaeryang Baek 30d02c638c refac: banners 2025-04-11 15:34:21 -07:00
Timothy Jaeryang Baek c5636ff68c refac 2025-04-11 15:27:25 -07:00
Timothy Jaeryang Baek c846a550fa refac 2025-04-11 15:19:47 -07:00
Timothy Jaeryang Baek 59dd798156 refac/security: set ac to private by default 2025-04-11 14:46:02 -07:00
Jarrod Lowe a8bbaa61bd Merge branch 'dev' into main 2025-04-11 22:14:41 +12:00
Timothy Jaeryang Baek 5fea0a2221 refac: styling 2025-04-10 21:52:03 -07:00
Timothy Jaeryang Baek 6d63337ee9 refac 2025-04-10 21:22:14 -07:00
Timothy Jaeryang Baek b97ea72616 refac: tool call result display 2025-04-10 20:06:00 -07:00
Timothy Jaeryang Baekanddulicon 1921420319 refac
Co-Authored-By: dulicon <57261058+dulicon@users.noreply.github.com>
2025-04-10 20:01:07 -07:00
Timothy Jaeryang Baek a47a958edd refac: rename functions 2025-04-10 19:43:26 -07:00
Timothy Jaeryang Baek 53df5d3946 refac: rename hard to understand cryptic function names 2025-04-10 19:41:17 -07:00
Timothy Jaeryang Baek 0f27968ceb fix: tool server session auth 2025-04-10 17:47:07 -07:00
Tim Jaeryang Baek 36ac81b229 Merge pull request #12727 from decent-engineer-decent-datascientist/main
feat: add Azure AI Speech STT provider
2025-04-10 16:50:40 -07:00
priten 9a55257c5b feat: add Azure AI Speech STT provider
- Add Azure STT configuration variables for API key, region and locales
- Implement Azure STT transcription endpoint with 200MB file size limit
- Update Audio settings UI to include Azure STT configuration fields
- Handle Azure API responses and error cases consistently
2025-04-10 15:38:59 -05:00
Timothy Jaeryang Baek 05aa9c6d9c refac 2025-04-10 12:27:11 -07:00
Tim Jaeryang Baek f0efee5de4 Merge pull request #12603 from alpha-pet/fix-convert-ogg-container-openai-transcription
fix: Convert ogg to wav for OpenAI transcription endpoint
2025-04-10 12:25:46 -07:00
Timothy Jaeryang Baek bf0716e85e refac: styling 2025-04-10 12:05:01 -07:00
Timothy Jaeryang Baek f9bd49bf56 refac 2025-04-10 11:18:53 -07:00
Tim Jaeryang Baek 4cbf16c456 Merge pull request #12717 from Kheiden/progressive-webapp-enhancements
feat: Add PWA external manifest URL env var
2025-04-10 10:55:54 -07:00
Timothy Jaeryang BaekandRobert Piosik 1eba6a812e enh: smaller code text size
Co-Authored-By: Robert Piosik <robertpiosik@gmail.com>
2025-04-10 10:50:46 -07:00
Timothy Jaeryang Baek 3f71929a69 refac: styling 2025-04-10 10:46:44 -07:00
Timothy Jaeryang Baek d8780e4de2 feat: detect artifacts toggle 2025-04-10 10:36:57 -07:00
Kurt Heiden c12f8d28b9 Add PWA external manifest URL env var 2025-04-10 11:35:43 -06:00
Timothy Jaeryang Baek d4297a3e3e refac: styling 2025-04-10 10:31:38 -07:00
Timothy Jaeryang Baek fb0ee1329d enh: tool server data timeout 2025-04-10 10:23:14 -07:00
Timothy Jaeryang Baek 23dcae7054 refac: selector 2025-04-10 10:18:56 -07:00
Tim Jaeryang Baek 7aab8b9307 Merge pull request #12567 from aleprj/fix/ms-sso
fix: Make Microsoft SSO work even when custom signing keys. #12475
2025-04-10 10:11:49 -07:00
Timothy Jaeryang BaekandAnthony Durussel f10566f3de feat: allow events from pipelines
Co-Authored-By: Anthony Durussel <87324020+anthonydurussel@users.noreply.github.com>
2025-04-10 09:20:18 -07:00
Timothy Jaeryang Baek 0611eb8ac7 refac: layout load 2025-04-10 09:15:08 -07:00
Tim Jaeryang Baek a3e477e100 Merge pull request #12602 from alpha-pet/fix-tool-server-indexing-mismatch
fix: mismatch between TOOL_SERVERS / TOOL_SERVER_CONNECTIONS indexing
2025-04-10 09:06:30 -07:00
Tim Jaeryang Baek b40b50fb2d Merge pull request #12563 from IONOS-Productivity/oidc-add-support-for-pkce
OIDC add support for PKCE
2025-04-10 09:03:31 -07:00
Tim Jaeryang Baek e065073bea Merge pull request #12689 from asncx/fix-folder-chat-rename
fix: Chat rename not updating inside folders
2025-04-10 08:51:22 -07:00
Tim Jaeryang Baek 33b35b797c Merge pull request #12594 from gaby/search-files
feat: Add support for searching files by name
2025-04-10 08:50:13 -07:00
Tim Jaeryang Baek ef14d0b46d Merge pull request #12562 from almajo/fix/agrimm/correct_knowledge_base_citations
fix: Correct Citations for knowledge base models
2025-04-10 08:48:47 -07:00
Tim Jaeryang Baek 62ef0bad6f Merge pull request #12680 from lucyknada/patch-1
fix #12678
2025-04-10 08:46:41 -07:00
Timothy Jaeryang Baek 63e5200e2f refac 2025-04-10 08:46:12 -07:00
Tim Jaeryang Baek 1865e29bfc Merge pull request #12683 from Youggls/dev
feat: Add sougou web search engine.
2025-04-10 08:45:15 -07:00
Tim Jaeryang Baek f7dc6474ee Merge pull request #12694 from SadmL/dev
[i18n] Russian locale misspelling fix
2025-04-10 08:43:51 -07:00
Timothy Jaeryang Baek 536bc36e36 chore: bump duckduckgo 2025-04-10 08:42:03 -07:00
Taylor Wilsdon 708fcdc6b7 Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-10 06:32:17 -04:00
SadmL de926a060d [i18n] Russian locale misspelling fix 2025-04-10 13:17:52 +03:00
Thomas Rehn 4731e0d0e3 fix: convert webm to wav for OpenAI transcription endpoint 2025-04-10 09:00:51 +02:00
Youggls 3e2a6df1fb feat: Add sougou web search API for backend, add config panel in for frontend. 2025-04-10 14:51:44 +08:00
lucy bc295546cd fix #12678 2025-04-10 07:23:34 +02:00
Tim Jaeryang Baek 6c9a4d6ce6 Merge pull request #12606 from alpha-pet/feat-openapi-yaml-support
Feat: openapi yaml support
2025-04-09 18:37:09 -07:00
Tim Jaeryang Baek e27c5989bc Merge pull request #12657 from noamtamim/patch-1
docs: Set FastAPI app title to "Open WebUI"
2025-04-09 18:32:26 -07:00
Tim Jaeryang Baek 5f116d69c2 Merge pull request #12664 from Beni34/main
Updated Hungarian language to full. translation.json
2025-04-09 16:43:08 -07:00
Tim Jaeryang Baek a27dcc84a3 Merge pull request #12654 from valckmir/main
feat: disable persistent config
2025-04-09 16:16:14 -07:00
Beni34 7cee0073cf Update translation.json 2025-04-09 21:40:23 +02:00
Miroslav Valcicak a5faaf388e refactor: rename persistent config variable 2025-04-09 21:22:25 +02:00
asncx 87af2692e5 fix: folder chat rename 2025-04-09 20:56:49 +02:00
Noam Tamim 15fd822916 Set FastAPI title to Open WebUI
This affects the generated OpenAPI (`/docs` and `/openapi.json`).
2025-04-09 19:45:17 +03:00
Miroslav Valcicak 68aea6bf10 feat: disable persistent config 2025-04-09 16:18:58 +02:00
Tim Jaeryang Baek 38d5b03685 Merge pull request #12647 from behrmann/multimail
fix: choose the first mail if multiple are returned from LDAP
2025-04-09 03:47:49 -07:00
Jörg Behrmann 2e7a01f30a fix: choose the first mail if multiple are returned from LDAP 2025-04-09 10:53:02 +02:00
Tim Jaeryang Baek a363c0619a Merge pull request #12638 from panda44312/patch-12
i18n: zh-cn
2025-04-09 01:45:41 -07:00
Tim Jaeryang Baek f49d3d09f5 Merge pull request #12637 from TiancongLx/dev
i18n: update zh-TW
2025-04-09 01:45:31 -07:00
Thomas Rehn 17ea20d3ad feat: OpenAPI YAML support for frontend toolserver 2025-04-09 09:46:44 +02:00
Panda 92611b588c i18n: zh-cn 2025-04-09 09:44:10 +02:00
Tiancong Li de1e9f3c0f i18n: update zh-TW 2025-04-09 15:25:37 +08:00
Tim Jaeryang Baek d8c0a1227a Merge pull request #12552 from tth37/dev
i18n: update zh-CN
2025-04-08 22:17:57 -07:00
Tim Jaeryang Baek 84ea9ea011 Merge pull request #12555 from SadmL/dev
[i18n] Russian locale update
2025-04-08 22:17:50 -07:00
Tim Jaeryang Baek b130b55f5b Merge pull request #12571 from hurxxxx/feat/reindex-knowledge-files
feat: reindex knowledge files
2025-04-08 22:10:35 -07:00
Tim Jaeryang Baek 2ff1657a7a Merge pull request #12573 from gem85247/dev
fix: manifest.json cross-origin issues
2025-04-08 22:07:43 -07:00
Tim Jaeryang Baek 2575dac4ed Merge pull request #12604 from maurerle/ddg_improve_stacktrace
**fix** improve stack trace of duckduckgo exception
2025-04-08 13:03:57 -07:00
Tim Jaeryang Baek 8c3146f6f8 Merge pull request #12615 from robert-norberg/additional-RAG-logging
chore: Add debug logging to RAG utils
2025-04-08 13:03:38 -07:00
Tim Jaeryang Baek 1f12023d85 Merge pull request #12617 from Classic298/dev
feat: UVICORN_WORKERS variable and docs
2025-04-08 12:52:47 -07:00
Timothy Jaeryang Baek 63eab42208 refac 2025-04-08 12:51:48 -07:00
Timothy Jaeryang Baek 60d11c1f6f enh: password max length verification 2025-04-08 12:50:25 -07:00
DrMelone f17befc853 Update __init__.py 2025-04-08 18:55:07 +02:00
DrMelone 65b5c6c0c7 Update start_windows.bat 2025-04-08 18:52:12 +02:00
DrMelone 2c1d0e3857 Update start.sh 2025-04-08 18:50:07 +02:00
DrMelone 4c9a791b06 Update env.py 2025-04-08 18:47:48 +02:00
DrMelone f822d89350 Update env.py 2025-04-08 18:38:00 +02:00
Robert Norberg 2337b36609 add debug logging to RAG utils 2025-04-08 12:08:32 -04:00
Thomas Rehn 81af20aa9b refac: use logging instead of print 2025-04-08 15:16:55 +02:00
Thomas Rehn 27e8c6fce4 feat: add support for OpenAPI spec in YAML format 2025-04-08 15:16:52 +02:00
Thomas Rehn d99a883867 fix: convert ogg to wav for OpenAI transcription endpoint 2025-04-08 15:04:04 +02:00
Thomas Rehn f5b2867e45 fix: mismatch between TOOL_SERVERS / TOOL_SERVER_CONNECTIONS indexing 2025-04-08 14:08:08 +02:00
Florian Maurer 760ea3f4af duckduckgo: backend api has been deprecated since december
also increase duckduckgo-search version

see https://github.com/deedy5/duckduckgo_search/commit/3ee8e08b1c8efc9b7356f406db299f1114dc4860
2025-04-08 14:02:06 +02:00
Florian Maurer 337c7caafa improve stack trace of duckduckgo exception
* fix search_results out of scope
* ddgs.text does already always return a list
2025-04-08 13:52:23 +02:00
Haodong Tian d50ca0aa43 Update zh-CN translation.json 2025-04-08 15:49:20 +08:00
Haodong Tian 399e7b1670 Update zh-CN translation.json 2025-04-08 15:47:24 +08:00
Haodong Tian e92d3073bb Update zh-CN translation.json
- Minor fix
2025-04-08 15:37:50 +08:00
Haodong Tian 00f58658f7 i18n: Apply i18n to Web Search proxy tooltips 2025-04-08 07:01:56 +00:00
Haodong Tian b15adc63fe Update zh-CN translation.json
- Standardize Chinese translations for "autocomplete"
2025-04-08 14:24:39 +08:00
Juan Calderon-Perez fed47f2e2b Add content param to /search route 2025-04-08 01:00:06 -04:00
Juan Calderon-Perez 1c60b8d543 Rewrite logic 2025-04-08 00:56:21 -04:00
Juan Calderon-Perez 8609ca3657 Fix formatting again 2025-04-08 00:49:55 -04:00
Juan Calderon-Perez e06ff17a70 Fix formatting 2025-04-08 00:48:54 -04:00
Juan Calderon-Perez 7c1b004687 simplify logic 2025-04-08 00:44:47 -04:00
Juan Calderon-Perez 353b104c77 add support for searching files 2025-04-08 00:42:37 -04:00
Jarrod Lowe b4465987f9 Environment variable to redirect on logout 2025-04-08 13:39:00 +12:00
Tim Jaeryang Baek 66322727e3 Merge pull request #12580 from lainedfles/opentelemetry-requirements-fix
fix: Upgrade OpenTelemetry instrumentation packages to 0.52b1
2025-04-07 14:19:17 -07:00
Self Denial 0e71b96597 fix: Upgrade OpenTelemetry instrumentation packages to 0.52b1
This commit updates all OpenTelemetry instrumentation dependencies (FastAPI, SQLAlchemy, Redis, Requests, Logging, HTTPX, aiohttp-client) from version 0.52b0 to 0.52b1 to satisfy opentelemetry-sdk==1.31.1 dependencies.
2025-04-07 14:49:47 -06:00
Gilles M f3f45209e9 fix: manifest.json cross-origin
When utilizing Cloudflare Zero Trust, fetching the manifest.json file may encounter issues because the request is redirected through the Zero Trust authentication page.
2025-04-07 19:01:18 +02:00
hurxxxx e7715f3760 i18n: add reindex knowledge files dialog 2025-04-08 01:37:50 +09:00
hurxxxx 7c828015d3 fix: ReindexKnowledgeFilesConfirmDialog 2025-04-08 00:53:11 +09:00
hurxxxx 4e545d432b feat: add new admin func - reindex knowledge files 2025-04-08 00:44:10 +09:00
Taylor Wilsdon 66bf6d88bc Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-07 10:17:57 -04:00
aleprj e8f02328f4 Merge branch 'open-webui:main' into fix/ms-sso 2025-04-07 11:16:21 -03:00
aleprj 818d24e672 fix: add appid to the URL to ensure we fetch the correct parameters in all scenarios 2025-04-07 11:15:47 -03:00
Alexander Grimm 1e7a36478b ~ update to latest change 2025-04-07 15:16:47 +02:00
Alexander Grimm d8a7e9c660 fix: correct document citation 2025-04-07 15:16:47 +02:00
Thomas Lehmann 5c658a4879 feat(config): add config OAUTH_CODE_CHALLENGE_METHOD
Add support to enable OIDC code challenge method (PKCE).
2025-04-07 14:03:52 +02:00
SadmL b81789190e [i18n] Russian locale update 2025-04-07 12:04:58 +03:00
Haodong Tian f172403049 Update zh-CN translation.json 2025-04-07 16:11:54 +08:00
Timothy Jaeryang Baek 9cb1104f09 refac 2025-04-07 00:58:32 -07:00
Timothy Jaeryang Baek d1a77a9110 Merge pull request #12549 from ulagbulag/fix/500-when-unknown-models
fix: internal server error when calling completions API with non-existent model names
2025-04-07 00:58:02 -07:00
Ho Kim 1efae95a36 fix: internal server error when calling completions API with non-existent model names
Signed-off-by: Ho Kim <ho.kim@ulagbulag.io>
2025-04-07 06:48:39 +00:00
Timothy Jaeryang Baek 54a74b2c0a chore: bump 2025-04-06 23:46:59 -07:00
Timothy Jaeryang Baek 908492702a chore: bump 2025-04-06 23:46:40 -07:00
Timothy Jaeryang Baek 5553ec3e51 Merge pull request #12537 from open-webui/dependabot/npm_and_yarn/dev/tiptap/pm-2.11.7
build(deps): bump @tiptap/pm from 2.10.0 to 2.11.7
2025-04-06 23:45:59 -07:00
Timothy Jaeryang Baek ddec6cf0fa Merge pull request #12545 from mogorman/elixir_syntax_highlighting
feat: add Elixir highlighting
2025-04-06 23:45:30 -07:00
Timothy Jaeryang Baek 1857f152c2 Merge pull request #12548 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-04-06 23:45:07 -07:00
Timothy Jaeryang Baek c5deb5cda4 Merge pull request #12541 from open-webui/dependabot/uv/dev/opentelemetry-instrumentation-logging-0.52b0
build(deps): bump opentelemetry-instrumentation-logging from 0.51b0 to 0.52b0
2025-04-06 23:43:48 -07:00
Timothy Jaeryang Baek d3ed20e907 Merge pull request #12535 from open-webui/dependabot/pip/backend/dev/youtube-transcript-api-1.0.3
build(deps): bump youtube-transcript-api from 0.6.3 to 1.0.3 in /backend
2025-04-06 23:43:35 -07:00
Timothy Jaeryang Baek 82252cb760 Merge pull request #12532 from open-webui/dependabot/pip/backend/dev/chromadb-0.6.3
build(deps): bump chromadb from 0.6.2 to 0.6.3 in /backend
2025-04-06 23:43:08 -07:00
Timothy Jaeryang Baek 9e700dc9a2 Merge pull request #12533 from open-webui/dependabot/pip/backend/dev/pgvector-0.4.0
build(deps): bump pgvector from 0.3.5 to 0.4.0 in /backend
2025-04-06 23:42:30 -07:00
Timothy Jaeryang Baek e1f60feb7f Merge pull request #12529 from open-webui/dependabot/pip/backend/dev/azure-identity-1.21.0
build(deps): bump azure-identity from 1.20.0 to 1.21.0 in /backend
2025-04-06 23:41:53 -07:00
Timothy Jaeryang Baek aefb3117db fix: chat completion non-existent model issue 2025-04-06 23:40:48 -07:00
Aleix Dorca 76d8935d0b Update catalan translation.json 2025-04-07 08:31:44 +02:00
dependabot[bot] dbc06084a7 build(deps): bump opentelemetry-instrumentation-logging
Bumps [opentelemetry-instrumentation-logging](https://github.com/open-telemetry/opentelemetry-python-contrib) from 0.51b0 to 0.52b0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python-contrib/commits)

---
updated-dependencies:
- dependency-name: opentelemetry-instrumentation-logging
  dependency-version: 0.52b0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:43:31 +00:00
Matthew O'Gorman 8a81f5f188 add support for elixir code syntax highlighting 2025-04-06 23:42:53 -04:00
dependabot[bot] ba66995532 build(deps): bump @tiptap/pm from 2.10.0 to 2.11.7
Bumps [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) from 2.10.0 to 2.11.7.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/pm@2.11.7/packages/pm/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/pm@2.11.7/packages/pm)

---
updated-dependencies:
- dependency-name: "@tiptap/pm"
  dependency-version: 2.11.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:44 +00:00
dependabot[bot] 96ecdc37b3 build(deps): bump youtube-transcript-api from 0.6.3 to 1.0.3 in /backend
Bumps [youtube-transcript-api](https://github.com/jdepoix/youtube-transcript-api) from 0.6.3 to 1.0.3.
- [Release notes](https://github.com/jdepoix/youtube-transcript-api/releases)
- [Commits](https://github.com/jdepoix/youtube-transcript-api/compare/v0.6.3...v1.0.3)

---
updated-dependencies:
- dependency-name: youtube-transcript-api
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:21 +00:00
dependabot[bot] f187a1c9ef build(deps): bump pgvector from 0.3.5 to 0.4.0 in /backend
Bumps [pgvector](https://github.com/pgvector/pgvector-python) from 0.3.5 to 0.4.0.
- [Changelog](https://github.com/pgvector/pgvector-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgvector/pgvector-python/compare/v0.3.5...v0.4.0)

---
updated-dependencies:
- dependency-name: pgvector
  dependency-version: 0.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:17 +00:00
dependabot[bot] 9e8b7ac611 build(deps): bump chromadb from 0.6.2 to 0.6.3 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/0.6.2...0.6.3)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 0.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:14 +00:00
dependabot[bot] 5379c08b9d build(deps): bump azure-identity from 1.20.0 to 1.21.0 in /backend
Bumps [azure-identity](https://github.com/Azure/azure-sdk-for-python) from 1.20.0 to 1.21.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/esrp_release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-identity_1.20.0...azure-identity_1.21.0)

---
updated-dependencies:
- dependency-name: azure-identity
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:11 +00:00
Timothy Jaeryang Baek 63533c9e3a Merge pull request #12524 from open-webui/dev
0.6.2
2025-04-06 20:41:10 -07:00
Timothy Jaeryang Baek 8e8bc3125e doc: changelog 2025-04-06 20:41:03 -07:00
Timothy Jaeryang Baek 2c083f58d9 chore: format 2025-04-06 20:36:24 -07:00
Timothy Jaeryang Baek 93962f01cc doc: changelog 2025-04-06 20:34:47 -07:00
Timothy Jaeryang Baek 40d019ff50 refac 2025-04-06 20:23:23 -07:00
Timothy Jaeryang Baek 3885ea4a15 Merge pull request #12526 from gaby/dependabot-update
chore: Dependabot update
2025-04-06 19:46:03 -07:00
Juan Calderon-Perez bd28173de6 Track backend/requirements.txt updates 2025-04-06 22:33:34 -04:00
Timothy Jaeryang Baek 06820e701c refac: citations modal 2025-04-06 19:14:24 -07:00
Timothy Jaeryang Baek 2be08f27ea revert 2025-04-06 19:12:08 -07:00
Timothy Jaeryang Baek fabe5876c0 Merge pull request #12525 from TiancongLx/dev
i18n: update zh-TW
2025-04-06 18:57:19 -07:00
Tiancong Li f1a4afeb8f i18n: update zh-TW 2025-04-07 09:28:20 +08:00
Timothy Jaeryang Baek 914eb49767 chore: include accelerate dependency 2025-04-06 17:44:05 -07:00
Timothy Jaeryang Baek a8bc0d65a0 chore: format 2025-04-06 17:36:16 -07:00
Timothy Jaeryang Baek cbe2056587 fix: audio file upload response issue 2025-04-06 17:31:50 -07:00
Timothy Jaeryang Baek 65ed76abe1 refac: embedding prefix 2025-04-06 17:17:24 -07:00
Timothy Jaeryang Baek bc17511a05 refac 2025-04-06 17:10:09 -07:00
Timothy Jaeryang Baek 3413dd8ae1 refac 2025-04-06 17:02:39 -07:00
Timothy Jaeryang Baek 03759e79a3 refac 2025-04-06 17:00:46 -07:00
Timothy Jaeryang Baek 20211a489f refac 2025-04-06 16:57:36 -07:00
Timothy Jaeryang Baek 5d2e725f81 refac 2025-04-06 16:53:30 -07:00
Timothy Jaeryang Baek 2722613cde refac 2025-04-06 16:43:58 -07:00
Timothy Jaeryang Baek f243e523a6 refac 2025-04-06 15:52:38 -07:00
Timothy Jaeryang Baek 155dbd5a66 refac 2025-04-06 15:45:48 -07:00
Timothy Jaeryang Baek 9825d03602 Merge pull request #12507 from Ithanil/fix_web_result_collection_source_ids
fix: fix web results all getting the same source id when using embedding and retrieval
2025-04-06 15:43:21 -07:00
Timothy Jaeryang Baek 66cd40f959 chore: format 2025-04-06 15:37:46 -07:00
Timothy Jaeryang Baek dc2f8ec8c6 refac 2025-04-06 15:37:27 -07:00
Timothy Jaeryang Baek bf118718b3 Merge pull request #12521 from gaby/optimize-workflow
chore: Only run GitHub workflows when needed
2025-04-06 15:24:20 -07:00
Timothy Jaeryang Baek 64a0b28dab refac 2025-04-06 15:23:40 -07:00
Timothy Jaeryang Baek 6751d68034 Merge pull request #12506 from Ithanil/fix_web_result_source_ids
fix: fix web results all getting the same source id when bypassing embedding and retrieval
2025-04-06 15:21:14 -07:00
Timothy Jaeryang Baek 6274f4d0d3 Merge pull request #12520 from gaby/bump-docker
chore: Bump Python Docker image to python3.12
2025-04-06 15:17:27 -07:00
Juan Calderon-Perez 624c5250b1 Optimize GitHub actions 2025-04-06 18:15:56 -04:00
Timothy Jaeryang Baek c1ff69721a refac 2025-04-06 15:14:57 -07:00
Timothy Jaeryang Baek 1e98ae7608 Merge pull request #12431 from gaby/fix-12237
feat: Allow making content optional when listing all files
2025-04-06 15:13:13 -07:00
Timothy Jaeryang Baek 1bde36e072 Merge pull request #12510 from newnol/patch-1
i18n language pack vietnamese
2025-04-06 15:12:09 -07:00
Juan Calderon-Perez 89e7913ff2 Bump Python base Docker image to 3.12 2025-04-06 18:10:38 -04:00
Timothy Jaeryang Baek 51bf9eaa88 Merge pull request #12493 from TiancongLx/dev
i18n: update zh-TW
2025-04-06 15:09:21 -07:00
Timothy Jaeryang Baek b8cf9b738d Merge pull request #12511 from panda44312/patch-11
i18n: zh-cn
2025-04-06 15:09:13 -07:00
Timothy Jaeryang Baek 635c08a5ab Merge pull request #12517 from Ithanil/only_keep_retrieved_urls
fix: only keep URLs as sources for which the content was actually retrieved
2025-04-06 15:08:47 -07:00
Timothy Jaeryang Baek 6323b9f4f0 Merge pull request #12515 from gaby/fix-dependabot
chore: Fix dependabot configuration
2025-04-06 13:41:18 -07:00
Juan Calderon-Perez 94453839ba Update index.ts 2025-04-06 16:02:16 -04:00
Jan Kessler a506a1a61e only keep URLs as sources for which the content could actually be retrieved 2025-04-06 20:31:12 +02:00
Juan Calderon-Perez d266490862 Fix dependabot configuration 2025-04-06 13:58:17 -04:00
Juan Calderon-Perez ff1d454bdd Fix formatting 2025-04-06 13:32:51 -04:00
Juan Calderon-Perez eda3eba084 Merge branch 'open-webui:main' into fix-12237 2025-04-06 13:30:37 -04:00
Taylor Wilsdon 0a2747bc6a Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-06 13:01:39 -04:00
Panda 60c6e00d9f i18n: fix 2025-04-06 16:47:06 +02:00
Panda 246c839ee6 i18n: zh-cn 2025-04-06 16:42:09 +02:00
Ngô Tấn Tài 2994c58fd9 Update translation.json 2025-04-06 21:26:58 +07:00
Jan Kessler 4476060044 fix web results all getting the same source id when using embedding and retrieval 2025-04-06 15:51:05 +02:00
Jan Kessler 2729d8aab4 fix web results all getting the same source id when bypassing embedding and retrieval 2025-04-06 13:37:42 +02:00
Tiancong Li 26e1bfc772 Merge remote-tracking branch 'upstream/dev' into dev
* upstream/dev:
  refac
  Fallback from desc to summary to placeholder
2025-04-06 10:33:01 +08:00
Timothy Jaeryang Baek 9ea6cea0ba refac 2025-04-05 14:31:22 -07:00
Timothy Jaeryang Baek 71625663c0 Merge pull request #12496 from dan-sullivan/fix-12479/add-desc-to-openapi-tools
fix: openapi tools should pass description to model not summary
2025-04-05 15:30:19 -06:00
Tiancong Li 33451cf66d i18n: update zh-TW 2025-04-06 04:46:31 +08:00
Timothy Jaeryang Baek 41aca84913 Merge pull request #12491 from open-webui/dev
refac
2025-04-05 10:24:32 -07:00
Timothy Jaeryang Baek 5296ee0f71 refac 2025-04-05 11:24:05 -06:00
Timothy Jaeryang Baek da94835165 Merge pull request #12373 from open-webui/dev
0.6.1
2025-04-05 10:15:32 -07:00
Timothy Jaeryang Baek 13dfca4e76 chore: bump 2025-04-05 11:13:30 -06:00
Timothy Jaeryang Baek d5063f47f2 doc: changelog 2025-04-05 11:11:37 -06:00
Timothy Jaeryang Baek ef787e4a79 Merge pull request #12486 from FabioPolito24/text-file-handling-docling
fix: text file handling with docling
2025-04-05 09:55:51 -07:00
Timothy Jaeryang Baek 48d690c55d Merge pull request #12481 from gaby/generic-errors
fix: Improve auth error messages
2025-04-05 09:47:52 -07:00
Timothy Jaeryang Baek 69c68df34b Merge pull request #12480 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-04-05 09:47:39 -07:00
Timothy Jaeryang Baek b3110ca291 Merge branch 'dev' into dev 2025-04-05 09:47:30 -07:00
Fabio Polito cd0a1b4852 fix: fix for text file handling with docling 2025-04-05 16:44:08 +00:00
Timothy Jaeryang Baek 32f309b41d chore: format 2025-04-05 10:42:21 -06:00
Timothy Jaeryang Baek d23f757c1f refac 2025-04-05 10:41:46 -06:00
Juan Calderon-Perez 324550423c Fix formatting issues 2025-04-05 10:03:24 -04:00
Juan Calderon-Perez b4277c7d2e Make auth error messages generic 2025-04-05 09:56:11 -04:00
Kylapaallikko 84788b73ce Update translation.json 2025-04-05 16:37:54 +03:00
dan-sullivan 09344bbb80 Fallback from desc to summary to placeholder 2025-04-05 14:18:14 +01:00
Timothy Jaeryang Baek fe4f760b4a refac 2025-04-05 05:57:25 -06:00
Timothy Jaeryang Baek 807b208418 refac 2025-04-05 05:55:56 -06:00
Timothy Jaeryang Baek ae484e8edf refac 2025-04-05 05:45:32 -06:00
Timothy Jaeryang Baek 56dc7c5640 refac 2025-04-05 05:38:46 -06:00
Timothy Jaeryang Baek e570a98bf7 refac: substandard codebase overhauled 2025-04-05 05:31:22 -06:00
Timothy Jaeryang Baek 93bb77ede3 refac 2025-04-05 05:03:15 -06:00
Timothy Jaeryang Baek 66db2e1515 refac: tools removed UNNECESSARY CODE 2025-04-05 04:59:11 -06:00
Timothy Jaeryang Baek c9e9ce931b refac 2025-04-05 04:49:07 -06:00
Timothy Jaeryang Baek 9747a0e1f1 refac: tool servers 2025-04-05 04:40:01 -06:00
Timothy Jaeryang Baek 61778c8f71 Merge pull request #12478 from theredcat/main
Improve FR translation
2025-04-05 03:22:54 -07:00
Timothy Jaeryang Baek 579aca65bb Merge pull request #12477 from Ithanil/improved_rag_template
fix: more coherent default RAG template
2025-04-05 03:22:41 -07:00
Timothy Jaeryang Baek ee44383c83 refac 2025-04-05 04:12:57 -06:00
Timothy Jaeryang Baek 0c0505e1cd refac 2025-04-05 04:05:52 -06:00
Nathan DELHAYE 1b8557ce86 i18n: improve fr translation 2025-04-05 12:04:12 +02:00
Jan Kessler e0da6003f1 tweak default rag template to be more coherent and improve consistency with citations 2025-04-05 11:40:48 +02:00
Timothy Jaeryang Baek 0f310b3509 Merge pull request #12476 from Phlogi/dev-hybrid-search
fix: Avoid secondary error when collection is empty in parallel hybrid search
2025-04-05 02:10:29 -07:00
Phlogi 8cf8121812 Update utils.py
Avoid running any tasks for collections that failed to fetch data (have assigned None)
2025-04-05 10:41:21 +02:00
Timothy Jaeryang Baek a2f2203325 Merge pull request #12426 from silentoplayz/fix-feedback
fix: Conditionally hide export buttons
2025-04-05 01:30:51 -07:00
Timothy Jaeryang Baek 193a927aba fix: temp chat 2025-04-05 02:16:35 -06:00
Timothy Jaeryang Baek 4ad10f0c6e chore: format 2025-04-05 01:31:45 -06:00
Timothy Jaeryang Baek 5e06b6d3f4 refac: styling 2025-04-05 01:24:32 -06:00
Timothy Jaeryang Baek cb94a87cb0 refac: tools styling 2025-04-05 01:05:47 -06:00
Timothy Jaeryang Baek 793aa307ef Merge pull request #12433 from gaby/fix-allowed-endpoints
fix: API Key Endpoint Restrictions for Dynamic Paths
2025-04-04 08:24:10 -07:00
Timothy Jaeryang Baek 7e46cc4562 Merge pull request #12439 from narutopden/i18n-add-Tibetan-translation
i18n: add Tibetan language
2025-04-04 08:20:25 -07:00
Timothy Jaeryang Baek b612af29e9 refac 2025-04-04 09:17:19 -06:00
Timothy Jaeryang Baek feaf434d4e refac: input prompt 2025-04-04 09:11:54 -06:00
Timothy Jaeryang Baek 77fa11098a enh: openapi tool server custom path 2025-04-04 08:31:13 -06:00
Timothy Jaeryang Baek a32bb850fa Merge pull request #12450 from mahenning/fix-missing-parameter-rag
Fix: Add missing parameter for query_doc_with_hybrid_search
2025-04-04 07:00:16 -07:00
Timothy Jaeryang Baek 64b68b3641 Merge pull request #12447 from floriankick/fix-openai-api-empty-message-after-tool-call
fix: make content parameter optional in OpenAI chat completion API endpoint
2025-04-04 06:59:12 -07:00
Marko Henning 3b2b6e183d Added missing parameter for query_doc_with_hybrid_search. 2025-04-04 15:30:57 +02:00
Juan Calderon-Perez 138e985d66 Rename field to include_content 2025-04-04 08:38:24 -04:00
Florian Kick ec3435de00 make content parameter optional in OpenAI chat completion API endpoint 2025-04-04 13:30:51 +02:00
narutopden a25d87607e i18n: add Tibetan language 2025-04-04 14:55:49 +08:00
Juan Calderon-Perez 1c57e3e02c Fix API_KEY_ALLOWED_ENDPOINTS 2025-04-03 23:52:10 -04:00
Silentoplayz 41b9ff9e00 fix: funcs, models, prompts, & tools conditional export button 2025-04-03 23:33:30 -04:00
Juan Calderon-Perez 6d5cb6b04e Add query param to remove content from GET /api/v1/files 2025-04-03 23:02:24 -04:00
silentoplayz 1366822f99 Update Feedbacks.svelte
fix
2025-04-04 02:38:03 +00:00
Silentoplayz 2b0d9c26eb fix: don't show export button if nothing to export 2025-04-03 22:20:40 -04:00
Timothy Jaeryang Baek be20e6dec0 refac: message edit 2025-04-03 13:43:45 -06:00
Timothy Jaeryang Baek a1f3300767 fix: tls cert requirement 2025-04-03 10:01:03 -07:00
Timothy Jaeryang Baek dfdde8bb1f refac 2025-04-03 09:40:26 -07:00
Timothy Jaeryang Baek ba77a72925 refac: styling 2025-04-03 01:00:40 -07:00
Timothy Jaeryang Baek bcf0a8791e refac: styling 2025-04-03 01:00:04 -07:00
Timothy Jaeryang Baek 100f5a57e7 refac 2025-04-02 23:56:15 -07:00
Timothy Jaeryang Baek 911321878f refac 2025-04-02 23:49:42 -07:00
Timothy Jaeryang Baek faa68fcdaa enh: image tool response 2025-04-02 23:46:39 -07:00
Jan Kessler 257ca45456 leave out @ in redis+sentine url when no username/password is provided 2025-04-03 08:24:24 +02:00
Timothy Jaeryang Baek 561b2c0b1a refac: styling 2025-04-02 21:23:45 -07:00
Timothy Jaeryang Baek 436e3ff7de refac 2025-04-02 20:42:23 -07:00
Timothy Jaeryang Baek c0711ba0c9 refac 2025-04-02 20:07:09 -07:00
Timothy Jaeryang Baek 5c5160c294 refac: remove None params 2025-04-02 19:56:39 -07:00
Timothy Jaeryang Baek 2277566ce1 fix: tool server api key not being sent 2025-04-02 19:52:12 -07:00
Timothy Jaeryang Baek 959995c715 refac: use selected model for merge response 2025-04-02 19:33:20 -07:00
Timothy Jaeryang Baek 7a1e10f3a7 refac: rm OAUTH_USE_PICTURE_CLAIM 2025-04-02 19:23:24 -07:00
Timothy Jaeryang Baek b15bf0dc70 refac 2025-04-02 19:17:26 -07:00
Timothy Jaeryang Baek 506950b03a Merge pull request #12376 from MushroomLamp-COB/main
feat: Add option to disable OAuth profile pictures
2025-04-02 19:16:36 -07:00
Timothy Jaeryang Baek 94353458a5 refac: settings ui styling 2025-04-02 19:01:39 -07:00
Timothy Jaeryang Baek 7eea95a48c feat: direct tools user permissions 2025-04-02 18:36:03 -07:00
Timothy Jaeryang Baek 94bf49440d enh: unload hybrid model if set to False 2025-04-02 18:15:14 -07:00
Timothy Jaeryang Baek 0644abe402 fix: admin folder deletion issue 2025-04-02 17:55:24 -07:00
Timothy Jaeryang Baek 9036945361 Merge pull request #12355 from silentoplayz/logging-fix
fix: update datetime and logging imports
2025-04-02 17:44:30 -07:00
silentoplayz dd5bafe8af Update env.py
Revert
2025-04-03 00:36:16 +00:00
CityOfBunbury 548c7f17d7 Added OAUTH_USE_PICTURE_CLAIM env var
Added OAUTH_USE_PICTURE_CLAIM to config.py

Added check to oauth.py on OAUTH_USE_PICTURE_CLAIM, to decide whether to user the profile picture in the claim or the default user.png
2025-04-03 08:24:14 +08:00
Timothy Jaeryang Baek bb7f4d408a Merge pull request #12364 from silentoplayz/archive-chats-option
feat: Added Archived Chats option in Chats settings section
2025-04-02 17:12:34 -07:00
Timothy Jaeryang Baek 6ee30042e7 Merge pull request #12353 from SadmL/dev
[i18n] Russian localization update
2025-04-02 15:47:44 -07:00
Timothy Jaeryang Baek 0554bbb1cb Merge pull request #12307 from paddy313/feature/mistral_ocr
feat: Added support for Mistral OCR for Content Extraction
2025-04-02 15:47:33 -07:00
Timothy Jaeryang Baek 517a57b7ca Merge pull request #12331 from panda44312/patch-10
i18n: zh-cn fix
2025-04-02 15:46:42 -07:00
Jan Kessler 35ea29b184 prepare websocket redis sentinel code for upcoming native support of sentinel in python-socketio 2025-04-02 21:50:00 +02:00
Silentoplayz ee68c9ea55 Update Chats.svelte
Added Archived Chats option in Chats settings section
2025-04-02 14:25:03 -04:00
Silentoplayz d65471c420 fix
my dev environment works again!
2025-04-02 11:28:45 -04:00
SadmL de8f94b008 [i18n] Russian localization update
Fixed typos and translated some lines
2025-04-02 18:13:52 +03:00
Patrick Wachter 0ac00b9256 refactor: update import path for MistralLoader 2025-04-02 13:56:10 +02:00
Timothy Jaeryang Baek d0db4756a6 Merge pull request #12310 from landerrosette/fix_filter_priority
fix: Filter function’s priority valve has no effect
2025-04-02 04:41:00 -07:00
Panda adaa614986 fix 2025-04-02 10:35:05 +02:00
Timothy Jaeryang Baek 5ab783001d Merge pull request #12304 from OriginalSimon/main
18n: Update Ukrainian translation
2025-04-01 16:12:59 -07:00
Timothy Jaeryang Baek 2a163996f9 Merge pull request #12294 from Xelaph/dev
**i18n**: Update Dutch translations
2025-04-01 16:12:48 -07:00
Timothy Jaeryang Baek 1b37bdcec8 Merge pull request #12278 from Saidoua/main
**i18n** Standard Arabic Translation
2025-04-01 16:11:54 -07:00
Timothy Jaeryang Baek 39b0c06630 Merge pull request #12299 from lowlyocean/rpi_onnxruntime
fix: Allow v0.6.0 to run on Raspberry Pi
2025-04-01 16:02:05 -07:00
landerrosette 2b7dd6e399 refactor: standardize filter valve retrieval logic 2025-04-02 02:58:02 +08:00
Patrick Wachter c5a8d2f857 refactor: update MistralLoader documentation and adjust parameters for signed URL retrieval 2025-04-01 20:14:34 +02:00
Patrick Wachter 93d7702e8c refactor: move MistralLoader to a separate module and just use the requests package instead of mistralai 2025-04-01 20:14:34 +02:00
Simon 0447d90951 Update translation.json 2025-04-01 20:01:08 +02:00
Daniel Nowak 8f8c34471e Pin onnxruntime to 1.20.1 to address SIGILL on certain arm64 hosts 2025-04-01 12:03:09 -04:00
Alex 883ad55f5c Update translation.json 2025-04-01 16:12:31 +02:00
Alex 82e5a6410c Update translation.json 2025-04-01 16:09:17 +02:00
Alex 88b93241db Update translation.json 2025-04-01 15:20:04 +02:00
Alex b652b8e5f3 Update translation.json 2025-04-01 14:25:04 +02:00
Patrick Wachter 1ac6879268 Add Mistral OCR integration and configuration support 2025-04-01 14:24:33 +02:00
Said Ouhdachi 825becceb0 Arabic Translation 2025-04-01 13:10:59 +02:00
Timothy Jaeryang Baek fffa793d6d Merge pull request #12270 from TiancongLx/dev
i18n: update zh-TW
2025-04-01 02:29:00 -07:00
Timothy Jaeryang Baek 289d49dbe7 Merge pull request #12269 from panda44312/patch-9
i18n: zh-cn
2025-04-01 02:28:51 -07:00
Tiancong Li fa72c277db i18n: update zh-TW 2025-04-01 17:10:32 +08:00
Panda 8799ff9575 fix 2025-04-01 10:52:05 +02:00
Panda 85c8a9bd68 i18n: zh-cn 2025-04-01 10:47:52 +02:00
Timothy Jaeryang Baek 15f6e3af12 Merge pull request #12259 from weisser-dev/dev
[FEAT] adjust Translations for temporary chat
2025-04-01 00:59:27 -07:00
Timothy Jaeryang Baek c79fa4214f Merge pull request #12262 from lukasz-pekala/fb/fixes-to-polish-translation
i18n: fixes to polish translation
2025-04-01 00:59:08 -07:00
Łukasz Pękala 29d5745335 fix: update Polish translation for "Capture" to improve accuracy 2025-04-01 09:14:29 +02:00
Erik 70bb0562f3 Merge branch 'open-webui:dev' into dev 2025-04-01 09:09:33 +02:00
Erik c8210d4043 upated like in PR discussed 2025-04-01 09:06:15 +02:00
Łukasz Pękala 9f470a4ff5 fix: update Polish translations to sound more natural 2025-04-01 08:53:12 +02:00
Timothy Jaeryang Baek cea5c1a63b Merge pull request #12254 from open-webui/dependabot/pip/backend/dev/einops-0.8.1
build(deps): bump einops from 0.8.0 to 0.8.1 in /backend
2025-03-31 23:45:23 -07:00
Timothy Jaeryang Baek 4ce05ad279 Merge pull request #12255 from open-webui/dependabot/pip/backend/dev/pypandoc-1.15
build(deps): bump pypandoc from 1.13 to 1.15 in /backend
2025-03-31 23:45:15 -07:00
Timothy Jaeryang Baek f3c2614b6c Merge pull request #12253 from open-webui/dependabot/pip/backend/dev/python-multipart-0.0.20
build(deps): bump python-multipart from 0.0.18 to 0.0.20 in /backend
2025-03-31 23:44:40 -07:00
Timothy Jaeryang Baek 79cca06e0c Merge pull request #12260 from weisser-dev/feat-default-value-for-enforce-temp-chat
[improvement] default permission for new groups is false for enforce …
2025-03-31 23:43:08 -07:00
Łukasz Pękala b60beb6ff6 fix: improve Polish translations for clarity and accuracy 2025-04-01 08:39:36 +02:00
Erik 295c7eb4c7 [improvement] default permission for new groups is false for enforce temp chat 2025-04-01 08:27:29 +02:00
Łukasz Pękala 361584b4fd fix: update Polish translations for clarity and accuracy 2025-04-01 08:26:21 +02:00
Erik abba7c128c [FEAT]-Adjust Translations for temporary chat 2025-04-01 08:21:03 +02:00
Timothy Jaeryang Baek e0ec2cdeb0 refac: $user 2025-03-31 20:32:12 -07:00
dependabot[bot] bb0e11bb97 build(deps): bump pypandoc from 1.13 to 1.15 in /backend
Bumps [pypandoc](https://github.com/JessicaTegner/pypandoc) from 1.13 to 1.15.
- [Release notes](https://github.com/JessicaTegner/pypandoc/releases)
- [Changelog](https://github.com/JessicaTegner/pypandoc/blob/master/release.md)
- [Commits](https://github.com/JessicaTegner/pypandoc/compare/v1.13...v1.15)

---
updated-dependencies:
- dependency-name: pypandoc
  dependency-version: '1.15'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 02:22:16 +00:00
dependabot[bot] fc5c8dbe3f build(deps): bump einops from 0.8.0 to 0.8.1 in /backend
Bumps [einops](https://github.com/arogozhnikov/einops) from 0.8.0 to 0.8.1.
- [Release notes](https://github.com/arogozhnikov/einops/releases)
- [Commits](https://github.com/arogozhnikov/einops/compare/v0.8.0...v0.8.1)

---
updated-dependencies:
- dependency-name: einops
  dependency-version: 0.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 02:22:13 +00:00
dependabot[bot] 1d30d8a13a build(deps): bump python-multipart from 0.0.18 to 0.0.20 in /backend
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.18 to 0.0.20.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.18...0.0.20)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 02:22:11 +00:00
Timothy Jaeryang Baek 04799f1f95 Merge pull request #11728 from open-webui/dev
0.6
2025-03-31 18:47:18 -07:00
Timothy Jaeryang Baek 1b7c125f00 refac 2025-03-31 18:47:08 -07:00
Timothy Jaeryang Baek 13f7a4cf56 doc: wording 2025-03-31 18:43:27 -07:00
Timothy Jaeryang Baek 8f8d8ba27d doc: changelog 2025-03-31 18:40:49 -07:00
Timothy Jaeryang Baek dca68871e6 doc: wording 2025-03-31 18:39:30 -07:00
Timothy Jaeryang Baek da561c50d0 doc: wording 2025-03-31 18:36:08 -07:00
Timothy Jaeryang Baek 3662ecdeab doc: changelog 2025-03-31 18:34:42 -07:00
Timothy Jaeryang Baek 391dd33da3 chore: format 2025-03-31 17:59:21 -07:00
Timothy Jaeryang Baek 0bc5441d72 refac: styling 2025-03-31 17:58:48 -07:00
Timothy Jaeryang Baek 5f792d2771 feat: enforced temporary chat 2025-03-31 17:58:43 -07:00
Timothy Jaeryang BaekandTaylor Wilsdon 580965df17 feat: public sharing permissions
Co-Authored-By: Taylor Wilsdon <6508528+taylorwilsdon@users.noreply.github.com>
2025-03-31 17:28:25 -07:00
Timothy Jaeryang Baek 50b3f47f81 feat: public sharing permissions 2025-03-31 17:15:51 -07:00
Timothy Jaeryang Baek 3ba12e7a43 Merge pull request #12239 from Phlogi/dev-threads-on-hybrid
perf: parallelize hybrid search
2025-03-31 17:06:32 -07:00
Timothy Jaeryang Baek def5444ea3 feat: hide base model 2025-03-31 16:51:42 -07:00
Timothy Jaeryang Baek cafc5413f5 refac 2025-03-31 14:13:27 -07:00
Timothy Jaeryang Baek 3dc40030a1 Merge pull request #12236 from Phlogi/fix_prefix
Fix middleware.py missing prefix on embedding function
2025-03-31 14:02:49 -07:00
Timothy Jaeryang Baek efb63fb13f chore: format 2025-03-31 14:02:02 -07:00
Timothy Jaeryang Baek 598222b76f Merge pull request #12238 from rgaricano/dev
PR: Translation (es-ES) - Spanish v0.6.0
2025-03-31 14:01:27 -07:00
Timothy Jaeryang Baek fac848ad33 refac: rm log info 2025-03-31 14:00:40 -07:00
_00_ 098c794de3 Update translation.json
fix X typo
lines 4 & 304
2025-03-31 19:52:15 +02:00
_00_ 1571e3dc16 FIX: Translation (es-ES) - Spanish
Complete Revision of Spanish Translation (es-ES)

Revised spanish translation.
Standardization of terminology, vocabulary, and syntax.
Revision of incorrectly explained or defined terms.
2025-03-31 17:12:55 +02:00
_00_ a65c426e1d FIX: Translation (es-ES) - Spanish rev.
Complete Revision of Spanish Translation (es-ES)

Revised spanish translation.
Standardization of terminology, vocabulary, and syntax.
Revision of incorrectly explained or defined terms.

(added two lost recently updated entries)
2025-03-31 16:59:05 +02:00
Phlogi 9c64310db5 Run hybrid_search in parallel 2025-03-31 16:43:37 +02:00
_00_ 3c507e829f FIX: Translation (es-ES) - Spanish
Complete Revision of Spanish Translation (es-ES)

Revised spanish translation.
Standardization of terminology, vocabulary, and syntax.
Revision of incorrectly explained or defined terms.
2025-03-31 16:42:56 +02:00
Phlogi bc64b2a67f Fix middleware.py missing prefix on embedding function 2025-03-31 15:19:07 +02:00
Timothy Jaeryang Baek 6ac153c989 refac: styling 2025-03-31 03:45:36 -07:00
Timothy Jaeryang Baek 5ce6c8ced3 refac 2025-03-31 01:45:22 -07:00
Timothy Jaeryang Baek 9809ffb095 refac 2025-03-31 01:30:08 -07:00
Timothy Jaeryang Baek a5dbd2e8dd refac: knowledge file ac 2025-03-31 01:10:18 -07:00
Timothy Jaeryang Baek 021e25ade1 Merge pull request #11087 from tarmst/fix-files-access-control
fix: Add access control usage to files APIs
2025-03-31 01:05:08 -07:00
Timothy Jaeryang Baek 05dfb9cecd refac: floating buttons behaviour in shared chat 2025-03-31 00:57:16 -07:00
Timothy Jaeryang Baek effa87638d refac: prompt command behaviour 2025-03-31 00:48:09 -07:00
Timothy Jaeryang Baek 300b7dfcc0 fix: model import/export 2025-03-31 00:39:20 -07:00
Timothy Jaeryang Baek fb875c574c Merge pull request #12218 from aleixdorca/dev
Update catalan translation.json
2025-03-31 00:19:28 -07:00
Aleix Dorca 6e190bebe8 Update catalan translation.json 2025-03-31 09:12:48 +02:00
Timothy Jaeryang Baek e4cc9f2b4f refac: folders 2025-03-30 23:36:15 -07:00
Timothy Jaeryang Baek 337df80c47 refac: styling 2025-03-30 23:17:06 -07:00
Timothy Jaeryang Baek d542881ee4 refac 2025-03-30 21:55:20 -07:00
Timothy Jaeryang Baek 4b75966401 refac: embedding prefix var naming 2025-03-30 21:55:15 -07:00
Timothy Jaeryang Baek 433b5bddc1 Merge pull request #8594 from jayteaftw/main
feat: Support for instruct/prefixing embeddings
2025-03-30 21:54:44 -07:00
Timothy Jaeryang Baek 12101fd26d Merge pull request #11307 from wangtunan/fix/stop_task_func_need_use_async_await_syntax
fix: stopTask func need use async/await syntax
2025-03-30 21:45:32 -07:00
Timothy Jaeryang Baek 33f93371dc feat: user webhooks system settings 2025-03-30 21:31:16 -07:00
Timothy Jaeryang Baek d55735dc1e refac: rm profile image from feedback user object 2025-03-30 21:23:42 -07:00
Timothy Jaeryang Baek d41844c308 feat: prompt autocompletion user setting 2025-03-30 21:19:09 -07:00
Timothy Jaeryang Baek 50b8dec3ac fix/refac: hybrid search 2025-03-30 20:48:22 -07:00
Timothy Jaeryang Baek ce0d82b55f Merge pull request #12132 from Phlogi/dev-fetch-documents-once
Avoid multiple data fetching
2025-03-30 20:44:32 -07:00
Timothy Jaeryang Baek febc85361f Merge pull request #12191 from rgaricano/dev
Fix alignament buttons text
2025-03-30 20:42:21 -07:00
Timothy Jaeryang Baek d9bbdd8dca revert 2025-03-30 20:38:47 -07:00
Timothy Jaeryang Baek 9c00f3caf3 Merge pull request #12192 from Ithanil/avatar_compression
feat: adjust jpeg compression quality to 0.15 for avatar uploads
2025-03-30 20:35:31 -07:00
Timothy Jaeryang Baek ee9ff96c7d Merge pull request #12194 from TiancongLx/dev
i18n: update zh-TW
2025-03-30 17:19:23 -07:00
Tiancong Li 50a0751418 i18n: update zh-TW 2025-03-30 23:24:27 +08:00
Jan Kessler 7f5dc22464 adjust jpeg compression quality to 0.15 for avatar uploads 2025-03-30 14:14:14 +02:00
_00_ 5b8317270e Fix alignament buttons text
Fix alignament buttons text

Fixed align tag on user & admin settings

- correction of some align tag assigned to buttons that are right and
have to be left, this means that although it is not noticeable in
English, in other languages, especially if several lines appear, it is
obvious.
2025-03-30 13:36:07 +02:00
Timothy Jaeryang Baek b5909ffe97 enh: tool args parse json fallback 2025-03-30 02:39:23 -07:00
Timothy Jaeryang Baek 03e61e7a4a refac: styling 2025-03-30 00:47:45 -07:00
Timothy Jaeryang Baek 215f991326 refac 2025-03-30 00:44:09 -07:00
Timothy Jaeryang Baek 2cee5a4a03 chore: format 2025-03-30 00:21:00 -07:00
Timothy Jaeryang Baek 3be626bef3 feat: goto message 2025-03-29 17:48:57 -07:00
Timothy Jaeryang Baek c700126c17 refac: html rendering 2025-03-29 16:52:49 -07:00
Timothy Jaeryang Baek feaa01dc77 fix: ollama requests 2025-03-29 16:52:42 -07:00
Timothy Jaeryang Baek 34607736c6 Merge pull request #12170 from JunaidPinjari/fix/csv-autodetect-encoding
Fix: CSV loader encoding issue using autodetect_encoding=True
2025-03-29 15:23:55 -07:00
Timothy Jaeryang Baek 5b9b802793 refac 2025-03-29 15:23:02 -07:00
Timothy Jaeryang Baek da5ea7acb8 Merge pull request #12176 from JunaidPinjari/feat/file-upload-download-options
Support skipping ingestion and downloading files as attachments
2025-03-29 15:20:47 -07:00
Timothy Jaeryang Baek 08ff1d8d88 refac 2025-03-29 15:03:10 -07:00
Timothy Jaeryang Baek c2ff73c3e1 refac 2025-03-29 14:59:03 -07:00
Junaid Pinjari a805e033a5 Feat: Add optional ingest_file and as_attachment params to upload and download endpoints 2025-03-29 15:42:11 +05:30
Junaid Pinjari e782e7d3a7 Fix: CSV loader encoding issue using autodetect_encoding=True 2025-03-29 13:14:53 +05:30
Timothy Jaeryang Baek 66015bb341 Merge pull request #12168 from yak1ex/fix_inconsistent_value_check_in_onMessageHandler_in_Chat_svelte
fix: Inconsistent value check in onMessageHandler() in Chat.svelte
2025-03-28 22:12:57 -07:00
Timothy Jaeryang Baek 737f41dd2e refac 2025-03-28 13:18:44 -07:00
Timothy Jaeryang Baek 402d32ccfd refac 2025-03-28 13:17:43 -07:00
Timothy Jaeryang Baek 0413c747a9 refac: hide hybrid option with full context mode 2025-03-28 13:16:56 -07:00
Timothy Jaeryang Baek acd5b07fb4 enh: tool servers info modal 2025-03-28 12:40:56 -07:00
Timothy Jaeryang Baek 0a49c366f0 refac 2025-03-28 12:25:59 -07:00
Timothy Jaeryang Baek af2c5f3a18 refac 2025-03-28 12:23:25 -07:00
Timothy Jaeryang Baek 115f34069a refac: tool call display 2025-03-28 12:18:27 -07:00
Timothy Jaeryang Baek 1ac87c55ff chore: format 2025-03-28 11:47:14 -07:00
Timothy Jaeryang Baek b8c1bb0ec5 refac 2025-03-28 02:30:13 -07:00
Timothy Jaeryang Baek f8835d78d9 refac: tool call block display 2025-03-28 02:27:40 -07:00
Timothy Jaeryang Baek fd9641dcd1 refac 2025-03-28 01:20:45 -07:00
Timothy Jaeryang Baek 5656f030c4 refac: error handling 2025-03-28 00:52:13 -07:00
Timothy Jaeryang Baek 2fc8ace46d fix: error message persistence issue 2025-03-28 00:25:00 -07:00
Timothy Jaeryang Baek 2070d1096b refac 2025-03-28 00:07:00 -07:00
Timothy Jaeryang Baek 5f9679253e refac: openapi tool server support 2025-03-27 23:59:16 -07:00
Timothy Jaeryang Baek 359e6f2fd2 Merge pull request #12149 from TiancongLx/dev
i18n: update zh-TW
2025-03-27 23:49:56 -07:00
Tiancong Li f647e86346 i18n: update zh-TW 2025-03-28 14:38:43 +08:00
Timothy Jaeryang Baek 7fce3e2a9c Merge pull request #11148 from Ithanil/redis_sentinel
feat: Support for Redis Sentinel
2025-03-27 20:36:53 -07:00
Timothy Jaeryang Baek c137d3ad17 enh: allow json schema for format 2025-03-27 20:27:16 -07:00
Timothy Jaeryang Baek 424f007b10 fix: ldap certificate path should not be required 2025-03-27 19:55:41 -07:00
Timothy Jaeryang Baek 5e720cd84b refac: ollama response_format support 2025-03-27 19:26:08 -07:00
Timothy Jaeryang Baek 7845b2fbef Merge pull request #12122 from panda44312/patch-8
i18n: zh-cn
2025-03-27 14:48:45 -07:00
Phlogi 04bf9ddab2 Avoid multiple data fetching 2025-03-27 19:05:20 +01:00
Panda 72bef8798f i18n: zh-cn 2025-03-27 11:05:57 +01:00
Timothy Jaeryang Baek 4547453141 refac 2025-03-27 02:57:02 -07:00
Timothy Jaeryang Baek fc6d432c2f refac 2025-03-27 02:53:24 -07:00
Timothy Jaeryang Baek 038df1131e refac 2025-03-27 02:50:53 -07:00
Timothy Jaeryang Baek d1bc2cfa2f refac: external tools server support 2025-03-27 02:27:56 -07:00
Jan Kessler 0615c11a53 fix sentinel connection being attempted for non-sentinel redis 2025-03-27 10:23:35 +01:00
Timothy Jaeryang Baek 69dee19568 refac 2025-03-27 01:42:33 -07:00
Timothy Jaeryang Baek 4a79320253 chore: format 2025-03-27 01:40:28 -07:00
Timothy Jaeryang Baek d9b6d78d5c feat: external tool server support frontend 2025-03-27 01:38:35 -07:00
Jan Kessler d0b13cf388 prefix sentinel envs with redis_ 2025-03-27 09:22:11 +01:00
Timothy Jaeryang Baek 3514a6c5ed Merge pull request #12050 from mahenning/fix-db-order
Fix: Normalize all database distances to score in [0, 1] (needs testing for different DBs)
2025-03-26 20:56:02 -07:00
Timothy Jaeryang Baek 7490bc9100 Merge branch 'dev' into fix-db-order 2025-03-26 20:55:42 -07:00
Timothy Jaeryang Baek eaba9d8a67 Merge pull request #11497 from mahenning/k_reranker
feat: Added new k_reranker parameter
2025-03-26 20:50:43 -07:00
Timothy Jaeryang Baek 9d834a8e90 Merge branch 'dev' into k_reranker 2025-03-26 20:50:31 -07:00
Timothy Jaeryang Baek 79e4f8a7bc refac 2025-03-26 20:47:52 -07:00
Timothy Jaeryang Baek 5119cc47fc refac 2025-03-26 20:46:55 -07:00
Timothy Jaeryang Baek 42636347e2 refac: start source citation from 1 2025-03-26 20:39:39 -07:00
Timothy Jaeryang Baek dd2d757270 Merge pull request #12090 from panda44312/patch-7
i18n: zh-cn
2025-03-26 20:37:27 -07:00
Timothy Jaeryang Baek 08cd749160 refac 2025-03-26 20:28:41 -07:00
Panda f2bc2f704e i18n: zh-cn 2025-03-26 15:14:16 +01:00
Timothy Jaeryang Baek a650d77af4 refac 2025-03-26 01:25:01 -07:00
Timothy Jaeryang Baek c062353a3f refac 2025-03-26 01:17:10 -07:00
Timothy Jaeryang Baek c1a02ec7d8 refac 2025-03-26 01:11:12 -07:00
Timothy Jaeryang Baek 1be6ad1250 feat: /messages chat endpoint support 2025-03-26 01:10:27 -07:00
Timothy Jaeryang Baek 7dcdb1b404 refac 2025-03-26 00:43:51 -07:00
Timothy Jaeryang Baek 5a7efad59c refac: tools 2025-03-26 00:40:24 -07:00
Timothy Jaeryang Baek 82ca88105c Merge pull request #12076 from TiancongLx/dev
i18n: update zh-TW
2025-03-25 23:50:27 -07:00
Tiancong Li ec08043f23 i18n: update zh-TW 2025-03-26 13:34:53 +08:00
Timothy Jaeryang Baek 823d9f0475 Merge pull request #12043 from Espinaydelfin/main
i18n: Agregar idioma Galego (Add Galician Language)
2025-03-25 21:02:16 -07:00
Timothy Jaeryang Baek 1a04028507 Merge pull request #12051 from PeterDaveHello/Update-zh-TW-locale
i18n: minor zh-TW Traditional Chinese translation fix
2025-03-25 21:01:12 -07:00
Timothy Jaeryang Baek 8fe4c51e26 Merge pull request #12054 from philipp-horstenkamp/main
i18n: Fixed typo in german translation
2025-03-25 21:00:57 -07:00
Timothy Jaeryang Baek 9ef5868226 Merge pull request #12056 from ivanbaldo/fix-tika-spaces
**fix**: Tika 3.1.0.0 sends a lot of blank lines which degrades the RAG results and don't look good on the UI, strip them.
2025-03-25 21:00:49 -07:00
Marko Henning 7531b7dcaa Satisfy github format check 2025-03-25 19:09:17 +01:00
Iván Baldo 115e46a6a2 Fix: Tika 3.1.0.0 sends a lot of blank lines which degrades the RAG results, strip them. 2025-03-25 14:53:14 -03:00
Philipp Horstenkamp ee263b2f2b Fixed typo in german translation 2025-03-25 17:19:35 +01:00
Peter Dave Hello ca50e339cd i18n: minor zh-TW Traditional Chinese translation fix 2025-03-26 00:04:33 +08:00
Marko Henning 94d9d3d590 Fix: Normalze all database distances to score in [0, 1] 2025-03-25 16:46:14 +01:00
Espinaydelfin 5cf730bba7 Agregar idioma Galego 2025-03-25 13:31:37 +01:00
Timothy Jaeryang Baek 4906af9319 refac: styling 2025-03-25 00:12:23 -07:00
Timothy Jaeryang Baek 0192b4b468 refac: styling 2025-03-25 00:10:09 -07:00
Timothy Jaeryang Baek 57d1527ce1 refac 2025-03-25 00:07:04 -07:00
Timothy Jaeryang Baek 48b89322fd Merge pull request #12009 from PeterDaveHello/Dockerfile
build: add one missing `--no-cache-dir` for `pip3 install` in Dockerfile
2025-03-24 11:44:25 -07:00
Timothy Jaeryang Baek 16018b75a3 Merge pull request #12016 from OriginalSimon/dev
18n: Update Ukrainian translation
2025-03-24 11:43:24 -07:00
Simon 0418416355 Update translation.json 2025-03-24 19:41:55 +01:00
Simon 1db46c31aa Merge branch 'open-webui:dev' into dev 2025-03-24 19:41:07 +01:00
Simon 4f06771559 Update translation.json 2025-03-24 19:40:30 +01:00
Timothy Jaeryang Baek 6ed617e9f5 Merge pull request #12006 from PeterDaveHello/Update-zh-TW-locale
i18n: Improve zh-TW Traditional Chinese translation
2025-03-24 11:36:54 -07:00
Timothy Jaeryang Baek f5590b2fc0 Merge pull request #12014 from PeterDaveHello/FixPullRequestTemplate
docs: fix spelling and grammatical issues in PR template
2025-03-24 11:36:40 -07:00
Timothy Jaeryang Baek 38d524f6a0 chore: format 2025-03-24 11:35:32 -07:00
Peter Dave Hello ed2ecca475 docs: fix spelling and grammatical issues in PR template 2025-03-25 00:26:21 +08:00
Peter Dave Hello aaf47486da build: add one missing --no-cache-dir for pip3 install in Dockerfile 2025-03-24 20:45:16 +08:00
Peter Dave Hello c1f1af97c1 i18n: Improve zh-TW Traditional Chinese translation 2025-03-24 04:27:14 +08:00
Timothy Jaeryang Baek d81d1333ba Merge pull request #11950 from shirayu/feat_auth_redirect
feat: Adding auth redirect parameter
2025-03-23 11:53:45 -07:00
Timothy Jaeryang Baek 9de3e6d716 Merge pull request #11971 from shirayu/remove_ENABLE_AUDIT_LOGS
Remove `ENABLE_AUDIT_LOGS`
2025-03-23 11:53:12 -07:00
Timothy Jaeryang Baek e5b7188379 refac: ollama only param 2025-03-23 11:50:40 -07:00
Timothy Jaeryang Baek a00e5a0c0e Merge pull request #11988 from binxn/dev
Updated middleware.py to add OpenRouter compatibility
2025-03-23 11:48:21 -07:00
Timothy Jaeryang Baek c94a1ec8f8 Merge pull request #11989 from MaxJa4/dev
i18n: Translation for de-DE German
2025-03-23 11:47:15 -07:00
Timothy Jaeryang Baek c714bd8739 refac 2025-03-23 11:45:55 -07:00
MaxJa4 f2866ed858 Adjust naming 2025-03-23 17:35:28 +01:00
MaxJa4 137f16a160 Rename chats 2025-03-23 17:35:23 +01:00
MaxJa4 73715538ed Fix spelling error 2025-03-23 17:35:23 +01:00
MaxJa4 e4078a6aee Add new translations 2025-03-23 17:35:23 +01:00
binxn efd86e2cb4 Updated middleware.py to add OpenRouter compatibility 2025-03-23 17:14:20 +01:00
Yak! c1f189a602 Fix inconsistent value check. 2025-03-23 17:52:48 +09:00
Yuta Hayashibe d144592660 chore: Remove ENABLE_AUDIT_LOGS and set the AUDIT_LOG_LEVEL NONE 2025-03-23 11:34:04 +09:00
Timothy Jaeryang Baek 75b18f92b9 refac 2025-03-22 14:01:07 -07:00
Timothy Jaeryang Baek 07a15494e5 Merge pull request #11955 from jfbloom22/dev
fix: Improve error handling in ChromaDB delete function for non-existent collections
2025-03-22 12:46:01 -07:00
Jonathan Flower bdd236fa3a improved error handling for deleting collections that do not exist in chromadb 2025-03-22 09:59:06 -04:00
Yuta Hayashibe 966940cb00 feat: Added redirect parameter to /auth 2025-03-22 15:04:46 +09:00
Timothy Jaeryang Baek f872bda19b Merge pull request #11936 from softue/auth-redirect
fix: redirection for users already logged in
2025-03-21 12:33:52 -07:00
Timothy Jaeryang Baek 22f6e0f2f4 refac 2025-03-21 08:08:15 -07:00
Timothy Jaeryang Baek 8aa6dade41 Merge pull request #11876 from mahenning/fix--rag-sorting
Fix: wrong citation order for chromadb, wrong order for hybrid search
2025-03-20 17:54:22 -07:00
Timothy Jaeryang Baek d047eb46cc refac 2025-03-20 17:54:13 -07:00
Timothy Jaeryang Baek 87a06a1976 fix: file delete from knowledge not working with bypass embedding 2025-03-20 17:46:11 -07:00
Timothy Jaeryang Baek a28436237c refac 2025-03-20 17:42:50 -07:00
Timothy Jaeryang Baek 558295f728 Merge pull request #11862 from hurxxxx/feat/auto-edit-new-folder
feat: Automatically enter edit mode when creating a new folder
2025-03-20 17:40:37 -07:00
Timothy Jaeryang Baek ca43a4e97a Merge pull request #11926 from djismgaming/spanish-translation-20250320
i18n: update Spanish translation
2025-03-20 17:24:36 -07:00
djismgaming cbd11cffa0 chore: update translation.json 2025-03-20 18:11:33 -04:00
Timothy Jaeryang Baek 1813bb509b Merge pull request #11830 from hurxxxx/feat/chat-title-rename-enter-submit
feat: submit chat title rename with Enter, cancel with ESC
2025-03-20 14:03:59 -07:00
Timothy Jaeryang Baek 1d305e7b2f chore: format 2025-03-20 14:02:33 -07:00
Timothy Jaeryang Baek 9b20ef4922 refac 2025-03-20 14:01:47 -07:00
Timothy Jaeryang Baek f066eea92e Merge pull request #11899 from genjuro214/set-playwright-timeout
perf: set shorter timeout for playwright and make it configurable
2025-03-20 14:00:59 -07:00
Timothy Jaeryang Baek b96557c46e refac: styling 2025-03-20 13:55:13 -07:00
Timothy Jaeryang Baek 13220991a4 Merge pull request #11846 from hurxxxx/feat/sidebar-chat-search-clear-button
feat: add clear ("X") button to sidebar chat search input
2025-03-20 13:50:58 -07:00
Timothy Jaeryang Baek 132a332ac7 Merge pull request #11915 from Alluuu/i18n-add-estonian-language
i18n: Add Estonian et-EE translations
2025-03-20 13:50:23 -07:00
Timothy Jaeryang Baek 42f3e40065 Merge pull request #11911 from diwakar-s-maurya/patch-1
fix: error message propagation from filter-pipeline
2025-03-20 13:48:36 -07:00
Rômulo Mendes Figueiredo c4c6e02b4c fix: redirection for users already logged in 2025-03-20 16:05:26 -03:00
Alluuu 5b276471b3 Added Estonian language translations.
Tried to organize language list, following existing pattern as best, as I could tell: English first, Alphabetical languages, finished by Chinese and Doge
2025-03-20 18:42:45 +02:00
Diwakar 2bdf77a726 Fix error message propagate from pipelines
Error message returned from pipelines was not being shown on UI. It showed "Connection closed". With this fix it will show the error message on the UI from the pipeline properly.
2025-03-20 22:20:27 +07:00
genjuro 07098c6352 perf: set shorter timeout for playwright and make it configurable 2025-03-20 15:28:09 +08:00
Marko Henning 5f48af5b91 Revert the ordering change with chromadb, not necessary with reranker results 2025-03-19 17:04:45 +01:00
Timothy Jaeryang Baek 70550e41fc enh: user groups/permissions endpoint 2025-03-19 08:47:31 -07:00
Timothy Jaeryang Baek c69d1c86fe enh: apply file size limit to knowledge 2025-03-19 08:36:41 -07:00
Timothy Jaeryang Baek f806ab0bd2 refac 2025-03-19 08:32:31 -07:00
hurxxxx 18a8a375ab fix: Enter edit mode with text pre-selected 2025-03-20 00:27:53 +09:00
Marko Henning ec8fc727b8 Fix wrong order for chromadb 2025-03-19 16:06:10 +01:00
Timothy Jaeryang Baek 6ace00df24 Merge pull request #11839 from tcgumus/patch-2
Turkish language pack updates
2025-03-19 06:24:44 -07:00
Timothy Jaeryang Baek b4c00b2eb2 Merge pull request #11833 from aleixdorca/dev
Update Catalan translation.json
2025-03-19 06:24:23 -07:00
Timothy Jaeryang Baek 4b10051326 Merge pull request #11832 from TiancongLx/dev
i18n: update zh-TW
2025-03-19 06:24:15 -07:00
Timothy Jaeryang Baek 01c236ee59 Merge pull request #11826 from panda44312/patch-6
i18n: zh-cn
2025-03-19 06:24:09 -07:00
Timothy Jaeryang Baek f28bec876e Merge pull request #11853 from genjuro214/fix-issue-11847
fix: correct incorrect usage of log.exception method
2025-03-19 06:22:56 -07:00
hurxxxx 11f2aaf7b1 feat: Automatically enter edit mode when creating a new folder 2025-03-19 21:55:15 +09:00
leilibj 3e8546135d fix: correct incorrect usage of log.exception method 2025-03-19 13:04:34 +08:00
hurxxxx 70610f8343 feat: add clear ("X") button to sidebar chat search input 2025-03-19 11:25:16 +09:00
Timothy Jaeryang Baek 6c4352de07 fix: table cells format 2025-03-18 17:53:11 -07:00
Tuna Çağlar Gümüş d68a6227ad Turkish language pack updates 2025-03-19 00:15:39 +03:00
Aleix Dorca bda5e0af74 Update Catalan translation.json 2025-03-18 20:08:16 +01:00
Tiancong Li 05fa67ae8f i18n: update zh-TW 2025-03-19 02:54:51 +08:00
hurxxxx 2b687e2c06 Use consistent function names. 2025-03-19 02:23:30 +09:00
hurxxxx 3b624f35ac feat: submit chat title rename with Enter, cancel with ESC 2025-03-19 02:00:58 +09:00
Marko Henning 5ab789e83e Add documentation on chroma special case 2025-03-18 16:44:58 +01:00
Marko Henning ba676b7ed6 Use k_reranker also for result merge, and add special sorting use case for ChromaDB 2025-03-18 16:25:24 +01:00
Panda 87a3a893e2 i18n: zh-cn 2025-03-18 15:54:03 +01:00
Timothy Jaeryang Baek 62e63cffe7 Merge pull request #11804 from djismgaming/spanish-translation-20250317
i18n: translated new language pairs to Spanish
2025-03-18 13:44:45 +00:00
Timothy Jaeryang Baek 20128e6aa8 Merge branch 'dev' into spanish-translation-20250317 2025-03-18 13:44:32 +00:00
Timothy Jaeryang Baek 9a9a8a4b28 Merge pull request #11808 from FarisHijazi/dev
FIX #5682 dates not surviving importing
2025-03-18 13:43:41 +00:00
Timothy Jaeryang Baek b609b9d297 chore: format 2025-03-18 06:39:42 -07:00
Timothy Jaeryang Baek 3186aeac08 chore: format 2025-03-18 06:39:37 -07:00
Marko Henning f13948d805 Fixed typo 2025-03-18 12:14:59 +01:00
Marko Henning c877b59cbc Address edge case with k < k_reranker, sort results for cutting off 2025-03-18 11:31:17 +01:00
Jan Kessler e68cd9b671 Redis Sentinel support for AppConfig 2025-03-18 09:30:18 +01:00
Jan Kessler 9167a8bef0 refac as prep for sentinel support in AppConfig 2025-03-18 08:44:50 +01:00
Jan Kessler 9bf663934a fixes & add envs for Sentinel implementation 2025-03-18 07:49:57 +01:00
Jan Kessler 4370332e32 second part of adding Redis Sentinel support 2025-03-18 07:49:57 +01:00
Jan Kessler 3b357746d8 first part of adding Redis Sentinel support 2025-03-18 07:49:56 +01:00
Faris Hijazi eff1a23c19 fixed 5682 dates not surviving importing
Simply replaced the frontend svelte code that does the importing. One of
the keys were wrong, it was `convo['timestamp']` instead of
`convo['create_time']`
2025-03-18 09:23:52 +03:00
djismgaming e2f6cdcf31 translated new languague pairs to Spanish 2025-03-17 23:18:24 -04:00
Timothy Jaeryang Baek f5c5a4119f Merge pull request #11785 from SadmL/dev
[i18n] Almost full Russian localization (rebased)
2025-03-18 01:28:02 +00:00
Timothy Jaeryang Baek 47d5e21061 Merge pull request #11786 from taylorwilsdon/scroll_enh
fix: Better visibility for scroll bar in webkit browsers
2025-03-18 01:25:58 +00:00
SadmL 9a55071d65 Update translation.json 2025-03-17 20:02:22 +03:00
Taylor Wilsdon 1119b06678 Widen scrollbar so that it can be clicked and dragged with a mouse or trackpad 2025-03-17 13:00:55 -04:00
Timothy Jaeryang Baek 54d5281256 refac 2025-03-15 18:41:04 +00:00
Timothy Jaeryang Baek 0777ab7011 refac 2025-03-15 18:28:02 +00:00
Timothy Jaeryang Baek d8eade736f refac 2025-03-15 18:23:40 +00:00
Timothy Jaeryang Baek b6e56c0e5a refac 2025-03-15 18:00:18 +00:00
Timothy Jaeryang Baek cfb2319923 refac: disable autocompletion by default 2025-03-15 17:40:39 +00:00
Timothy Jaeryang Baek 7de88cc4eb refac 2025-03-15 17:19:22 +00:00
Timothy Jaeryang Baek 10b9fa6ed0 refac 2025-03-15 17:16:19 +00:00
Timothy Jaeryang Baek 4f5aba8be7 refac 2025-03-15 17:13:40 +00:00
Timothy Jaeryang Baek d93828e923 enh: client-side pdf generation 2025-03-15 17:11:14 +00:00
Timothy Jaeryang Baek c57db1828f fix 2025-03-15 16:25:21 +00:00
Timothy Jaeryang Baek b5e74f4eae refac 2025-03-15 16:15:37 +00:00
Timothy Jaeryang Baek c8d02aafd9 Merge pull request #11697 from foraxe/fix-keep-alive-top-level
fix: Promote keep_alive to top-level in payload for Ollama API (dev branch)
2025-03-15 14:43:27 +00:00
Timothy Jaeryang Baek f1a84f45c8 Merge pull request #11699 from shaun-gallagher-octane/markdown-alert-renderer
feat: Markdown alert renderer
2025-03-15 02:12:16 +00:00
Timothy Jaeryang Baek 0551b90d47 Merge pull request #11611 from nikes/feat/pip-options
feat: pip options in tools/functions install requirements
2025-03-15 02:07:31 +00:00
Timothy Jaeryang Baek 2de03494a6 Merge pull request #11647 from kaytwo/multiline-valves
feat: allow string valves to include newlines
2025-03-15 02:05:56 +00:00
Timothy Jaeryang Baek aa37866798 chore: pyodide bump 2025-03-15 02:04:18 +00:00
Timothy Jaeryang Baek 7723705707 enh: always collapse code block 2025-03-15 02:01:59 +00:00
Timothy Jaeryang Baek dbf051ff4e enh: always expand details option 2025-03-15 01:56:04 +00:00
Shaun Gallagher 2065580a5f feat: Use AlertRenderer in blockquote evaluation 2025-03-14 15:22:56 -04:00
Shaun Gallagher 8bc797fe6a feat: Add renderer for Markdown alerts 2025-03-14 15:22:27 -04:00
foraxe 04046ed6c8 Alternative Temp Fix: Uncomment to Keep Alive Forever 2025-03-15 00:27:20 +08:00
foraxe 53448b3f0a Fix: Promote keep_alive to top-level in payload for Ollama API on dev branch 2025-03-15 00:08:05 +08:00
Timothy Jaeryang Baek 74da9c4e54 fix: firefox input height issue 2025-03-14 12:50:33 +00:00
Timothy Jaeryang Baek 4b1899bfca chore: dependency 2025-03-14 12:35:33 +00:00
Timothy Jaeryang Baek 8588b52fd6 Merge pull request #11605 from melroy89/update_fake_useragent
dependency: Update fake-useragent to v2.1.0
2025-03-14 11:52:30 +00:00
Timothy Jaeryang Baek 4025d61c33 Merge pull request #11636 from OliveiraHermogenes/fix-api-audio-mimetype-check
fix: audio api endpoint filetype check
2025-03-14 11:52:12 +00:00
Chris Kanich 53292e7d12 allow string valves to include newlines 2025-03-13 12:04:13 -05:00
Nikes b6664f5ae8 feat: pip options in tools/functions install requirements 2025-03-12 23:15:26 +02:00
Melroy van den Berg b02e6c6734 Deps: Update fake-useragent to v2.1.0 2025-03-12 20:41:51 +01:00
Timothy Jaeryang Baek d84e7d13d0 refac 2025-03-11 20:49:14 +00:00
Timothy Jaeryang Baek c309412980 enh: connection tags 2025-03-11 20:37:30 +00:00
Timothy Jaeryang Baek b427f506f6 fix 2025-03-11 20:12:34 +00:00
Timothy Jaeryang Baek ad51b0166c refac 2025-03-11 20:12:28 +00:00
Timothy Jaeryang Baek e1168139a6 refac 2025-03-11 19:30:09 +00:00
Timothy Jaeryang Baek 9e2fb73440 Merge pull request #11550 from silentoplayz/highlight-selected
feat: improve model dropdown highlight
2025-03-11 18:57:20 +00:00
Timothy Jaeryang Baek adfa67d9d2 Merge pull request #11507 from OrenZhang/feat_ot
feat(trace): opentelemetry instrument
2025-03-11 18:55:56 +00:00
Timothy Jaeryang Baek 396c28817c refac 2025-03-11 18:55:30 +00:00
Timothy Jaeryang Baek 7e0c5a44e3 Merge pull request #11552 from icsy7867/dev
Fixed knowledgebase view for decodeURIComponent
2025-03-11 18:53:17 +00:00
root 6fc9c69b9f Fixed knowledgebase view for decodeURIComponent 2025-03-11 14:44:04 -04:00
Timothy Jaeryang Baek 9adc1ebcc8 refac 2025-03-11 18:38:13 +00:00
Timothy Jaeryang Baek 6399aaf4b2 Merge pull request #11515 from davizucon/main
fix: url value when response contains url tag
2025-03-11 18:37:05 +00:00
Timothy Jaeryang Baek 16c383ab44 Merge pull request #11498 from tcgumus/patch-2
Add SSL Integration Comments and WebSocket Auto-Fix in start_windows.bat
2025-03-11 18:34:54 +00:00
Timothy Jaeryang Baek 8905d9e63b Merge pull request #11526 from rragundez/exlude-images-from-embeddings
Exlude images from embeddings
2025-03-11 18:32:38 +00:00
Timothy Jaeryang Baek 64fa37c15d Merge pull request #11545 from hurxxxx/fix/reset-taskId-after-conversation
fix: Reset taskId after completion to prevent incorrect cancellation behavior
2025-03-11 18:29:59 +00:00
Silentoplayz 2c99366543 feat: improve model dropdown highlight 2025-03-11 13:57:09 -04:00
Davi S. Zucon b1224b6839 fix code format 2025-03-11 12:40:31 -03:00
Davi S. Zucon 453200ca1e fix code format
fix code format
2025-03-11 12:39:15 -03:00
hurxxxx 1160e21d45 fix: reset taskId after conversation to prevent incorrect cancellation behavior 2025-03-11 21:18:53 +09:00
Timothy Jaeryang Baek 0e7164b4f5 Merge pull request #11520 from icsy7867/main
Added decodeURIcomponent function to several locations.
2025-03-11 10:33:41 +00:00
Rodrigo Agundez ac321f4b64 Add check if images and avoid trying to calculate embeddings 2025-03-11 12:50:44 +08:00
orenzhang 7bfda6652f feat(trace): optimize for trace env and instrument hooks 2025-03-11 11:53:03 +08:00
root 3fb477618e Added decodeURIcomponent function to several locations. 2025-03-10 17:40:40 -04:00
Tuna Çağlar Gümüş 9e7193b3d2 Update start_windows.bat 2025-03-10 22:37:57 +03:00
Davi S. Zucon 846509bf9f fix url value when response contains url tag
When the response contains b64 json, the "url" tag still exists but comes as null, the logic is to resolve this point where it contains the url tag, but empty, thus directing to b64_json
2025-03-10 15:59:08 -03:00
orenzhang c761e4fd08 feat(trace): opentelemetry instrument 2025-03-10 22:27:31 +08:00
Marko Henning 8b5b3f165a Add translation (partially) 2025-03-10 12:35:44 +01:00
Timothy Jaeryang Baek c2046b9247 refac 2025-03-10 11:16:30 +00:00
Tuna Çağlar Gümüş c5efeb2499 Add SSL Integration Comments and WebSocket Auto-Fix in start_windows.bat
This PR enhances the start_windows.bat script by:

Adding comments for SSL integration, making it easier for users to enable SSL when needed.
Including the -ws auto flag to address WebSocket-related issues, ensuring a smoother experience for users facing connection problems.
These improvements make the script more user-friendly and help troubleshoot common setup challenges.
2025-03-10 13:39:59 +03:00
Timothy Jaeryang Baek 116e0559f6 refac: oauth 2025-03-10 09:42:59 +00:00
Timothy Jaeryang Baek 428d89784b Merge pull request #11484 from xinhai-ai/mobile_touch
fix: mobile touch
2025-03-10 06:27:35 -03:00
星海 077c805880 fix: mobile touch 2025-03-10 15:43:53 +08:00
Timothy Jaeryang Baek 352ed67667 refac: prompt template behaviour 2025-03-10 01:15:45 +00:00
Timothy Jaeryang Baek ec29772068 refac: styling 2025-03-09 23:58:48 +00:00
Timothy Jaeryang Baek 3cc44b9de5 Merge pull request #11460 from silentoplayz/horizontal-scrol-tags2
feat: Horizontal scroll for tags in model dropdown selector
2025-03-09 20:58:05 -03:00
Timothy Jaeryang Baek f8ac44cfbd Merge pull request #11464 from FabioPolito24/docling_context_extraction_engine
feat: Docling context extraction engine
2025-03-09 20:57:48 -03:00
Timothy Jaeryang Baek 0c5a1e84e3 Merge pull request #11472 from thiswillbeyourgithub/better_frontmatter_pip_install3
fix: install all the requirements at the same time
2025-03-09 20:57:06 -03:00
Timothy Jaeryang Baek 1fe4d49ac6 Merge pull request #11466 from captn-hook/dev
fix: opensearch environment variables fix
2025-03-09 20:56:34 -03:00
Timothy Jaeryang Baek 41a83f1dc5 refac: code update behaviour 2025-03-09 23:49:26 +00:00
thiswillbeyourgithub 9e41b8b024 fix: install all the requirements at the same time
Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>
2025-03-09 23:38:23 +01:00
Tristan H 4c98508c0b Merge pull request #3 from open-webui/dev
Dev
2025-03-09 09:51:31 -07:00
Tristan H 084e2caf6f Update config.py to correctly handle environment strings
As seen in line 1536 just above this, strings need to be converted to bools in order to work.
2025-03-09 09:48:35 -07:00
Timothy Jaeryang Baek a75f82755d Update bug_report.yaml 2025-03-09 09:48:35 -07:00
Timothy Jaeryang Baek c1d2e0d999 refac 2025-03-09 09:48:35 -07:00
Timothy Jaeryang Baek 165e861a1d Create config.yaml 2025-03-09 09:48:35 -07:00
Timothy Jaeryang Baek fc056b1397 Update bug_report.yaml 2025-03-09 09:48:35 -07:00
Fabio Polito de0f158b04 Merge remote-tracking branch 'upstream/dev' into docling_context_extraction_engine
merge upstream dev
2025-03-09 16:12:43 +00:00
Fabio Polito 9d6743824e fix: fix params DoclingLoader 2025-03-09 16:12:14 +00:00
Silentoplayz ae1f0f0a3c feat: horizontal scroll for model dropdown tags 2025-03-09 11:41:14 -04:00
Timothy Jaeryang Baek cdb5eae43a refac 2025-03-09 15:05:13 +00:00
Timothy Jaeryang Baek 26980fb859 refac 2025-03-09 15:01:36 +00:00
Timothy Jaeryang Baekandrecrudesce 53a2acd541 enh: local, external, direct model list filter
Co-Authored-By: recrudesce <6450799+recrudesce@users.noreply.github.com>
2025-03-09 15:00:13 +00:00
Timothy Jaeryang Baek c58f0844a3 refac 2025-03-09 14:48:11 +00:00
Timothy Jaeryang Baek 5cc43ae880 enh: image panzoom 2025-03-09 14:39:54 +00:00
Timothy Jaeryang Baek 1c211e32b3 refac 2025-03-09 11:18:28 +00:00
Timothy Jaeryang Baek 0dce81f646 Merge pull request #11423 from silentoplayz/confirmation-prompts
feat: add more confirmation prompts
2025-03-09 07:40:36 -03:00
Timothy Jaeryang Baek b8863545d5 Merge pull request #11428 from silentoplayz/sticky-tags
feat: Sticky tags
2025-03-09 07:32:05 -03:00
Timothy Jaeryang Baek 36dd630057 Merge pull request #11444 from open-webui/main
dev
2025-03-09 07:31:02 -03:00
Silentoplayz 1de142f435 sticky tags 2025-03-08 18:00:06 -05:00
Hermógenes Oliveira e936d7b53d fix: audio api endpoint filetype check
RFC2046 allows the Content-Type field to have additional parameters
after the main type/subtype information (Section 1).

Following RFC4281, many applications put codec information inside
parameters in the Content-Type. This is especially common for formats
that support many codecs, such as Ogg (RFC5334, Section 4).

The `/api/audio/transcriptions` endpoint is currently rejecting files
that contain parameters in the Content-Type field with a bad request
error.

This commit changes the current check in order to accept any
Content-Type field that begins with a supported type/subtype as listed
in the `supported_filetypes` tuple.

Since Content-Type here is provided by the user, I believe this check
is meant to prevent honest mistakes, like posting a PDF to an audio
processing endpoint, not as a security measure against possibly
malicious use. Therefore, I think it's OK not to validate the rest of
the field.
2025-03-08 18:03:30 -03:00
Fabio Polito 0aa42615f9 Merge remote-tracking branch 'upstream/dev' into docling_context_extraction_engine
merge upstream
2025-03-08 18:52:51 +00:00
Silentoplayz d669932137 add confirmation prompts 2025-03-08 13:42:24 -05:00
Timothy Jaeryang Baek 6b806ae12f Merge pull request #11413 from hurxxxx/fix/global-filter-cannot-disable
fix: Prevent modification of global filter settings in "Admin panel" - "Settings" - "Models"
2025-03-08 14:15:47 -04:00
Timothy Jaeryang Baek b03fc97e28 Update bug_report.yaml 2025-03-08 18:13:28 +00:00
Timothy Jaeryang Baek d7bfa395b0 refac 2025-03-08 18:10:34 +00:00
Timothy Jaeryang Baek 67cd4f2ab1 Create config.yaml 2025-03-08 18:08:51 +00:00
Timothy Jaeryang Baek dc78d2f758 Update bug_report.yaml 2025-03-08 18:06:08 +00:00
Timothy Jaeryang Baek d40e696468 enh: multiple replica app state sync 2025-03-08 17:59:15 +00:00
Timothy Jaeryang Baek 324cd94c53 refac: user message delete 2025-03-08 17:03:08 +00:00
Timothy Jaeryang Baek 4e36b9794f fix: logit bias issue 2025-03-08 16:50:30 +00:00
Timothy Jaeryang Baek 58ff221d1f refac 2025-03-08 16:48:32 +00:00
Timothy Jaeryang Baek 1e36282708 refac 2025-03-08 16:23:20 +00:00
Timothy Jaeryang Baek 22b88f9593 Merge pull request #11324 from kela4/main
fix: opensearch vector db query structures, result mapping, filters, bulk query actions, knn_vector usage
2025-03-08 12:19:38 -04:00
Timothy Jaeryang Baek e4a79a0b0e Merge pull request #11394 from dannyl1u/fix/logit-bias-workspace-models
fix: logit_bias bug in workspace models
2025-03-08 12:11:53 -04:00
Timothy Jaeryang Baek 8375c2c32d Merge pull request #11397 from luke3butler/tavily-extract-depth
feat: Add environment variable to configure tavily extract depth
2025-03-08 12:11:37 -04:00
Timothy Jaeryang Baek e7013d45c5 Merge pull request #11409 from chengscott/patch-1
[feat] anonymous bind when LDAP_APP_DN is not specified
2025-03-08 12:11:13 -04:00
Timothy Jaeryang Baek ac507720c8 Merge pull request #11401 from TiancongLx/dev
i18n: update zh-TW
2025-03-08 12:10:58 -04:00
Timothy Jaeryang Baek d59433bf7e refac 2025-03-08 15:48:09 +00:00
hurxxxx 40257afeed fix: Using disabled attribute for elements is common 2025-03-09 00:12:37 +09:00
hurxxxx 851931fdf6 fix: Global filter cannot be modified in the model (changes have no effect) 2025-03-08 23:24:52 +09:00
Scott Cheng 8bd527be6a anonymous bind when LDAP_APP_DN is not specified 2025-03-08 21:19:21 +08:00
Tiancong Li e5b02f732b i18n: update zh-TW 2025-03-08 16:18:22 +08:00
Luke 7c4a9c2aea fix: correct type handling for TAVILY_EXTRACT_DEPTH configuration 2025-03-08 01:07:06 -05:00
Luke 7917128ed3 enh: enable configuration for tavily extract depth 2025-03-08 00:43:02 -05:00
dannyl1u 3c6a834ff4 fix: logit_bias in workspace models 2025-03-07 19:54:06 -08:00
Timothy Jaeryang Baek dc3abc2bcb refac 2025-03-08 00:13:58 +00:00
Timothy Jaeryang Baek 3ab917cdd0 Merge pull request #11325 from luke3butler/dev
feat: Add `tavily` as a `RAG_WEB_LOADER_ENGINE` option via extract API
2025-03-07 20:11:39 -04:00
Timothy Jaeryang Baek e3116aac76 Merge pull request #11361 from OriginalSimon/dev
18n: Update Ukrainian translation
2025-03-07 19:59:42 -04:00
Timothy Jaeryang Baek e85d69fe60 Merge branch 'dev' into dev 2025-03-07 19:59:33 -04:00
Timothy Jaeryang Baek 027c5c4b72 Merge pull request #11383 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-03-07 19:59:03 -04:00
Timothy Jaeryang Baek 88303093c0 Merge pull request #11384 from Peter-De-Ath/hotfix/tool-calls-params
fix: bug where default params in tools calls always used
2025-03-07 19:58:05 -04:00
Peter De-Ath 7e3be60f0e formating 2025-03-07 20:37:22 +00:00
Peter De-Ath 3b39807377 fix: updatepost_response_handler nativ tools handler to use parameters from properties fixing defaults always used 2025-03-07 20:07:36 +00:00
Kylapaallikko 17a4360566 Update translation.json 2025-03-07 19:44:56 +02:00
Timothy Jaeryang Baek 7a1cf1095a Merge pull request #11358 from OrenZhang/i18n_translation
i18n(common): add i18n translation
2025-03-07 13:33:59 -04:00
Timothy Jaeryang Baek ab92737a9a refac 2025-03-07 17:24:52 +00:00
Simon 27a6d1917d Update translation.json 2025-03-07 13:48:56 +01:00
Simon 25544b38bd Merge branch 'open-webui:dev' into dev 2025-03-07 13:42:46 +01:00
Simon cf449d1c7e Update translation.json 2025-03-07 13:42:05 +01:00
Simon 20826e15b3 Update translation.json 2025-03-07 13:31:56 +01:00
Timothy Jaeryang Baek d7a0437438 refac: model sort list 2025-03-07 12:16:16 +00:00
orenzhang b61ea393b4 refactor(lint): code lint 2025-03-07 20:03:53 +08:00
orenzhang dc04123330 Merge branch 'dev' into i18n_translation 2025-03-07 20:03:33 +08:00
orenzhang 72ea6dd9f1 refactor(lint): code lint 2025-03-07 19:59:09 +08:00
Timothy Jaeryang Baek f8f54237c6 Merge pull request #11290 from panda44312/patch-5
i18n: Update zh-cn
2025-03-07 06:06:40 -04:00
Timothy Jaeryang Baek 04921be9f1 Merge pull request #11287 from Youggls/dev
fix(chat): resolve duplicate collapsible IDs causing citation modal failures
2025-03-07 06:05:13 -04:00
Fabio Polito 1a066c0357 fix: fixed lock and pyproject 2025-03-07 00:32:02 +00:00
Fabio Polito 09ccd000d8 fix: fixing lock and pyproject 2025-03-07 00:27:59 +00:00
Fabio Polito e3eef58310 feat: merge with dev 2025-03-07 00:22:47 +00:00
Fabio Polito 17dbb3ec57 fix: fix typo 2025-03-07 00:19:26 +00:00
Peter De-Ath d862295a05 fix: update chat completion tools handler to use parameters from spec fixing defaults always used 2025-03-06 23:27:52 +00:00
Luke 987954c817 feat: Add Tavily extract web loader integration 2025-03-06 18:15:18 -05:00
Katharina 6cb0c0339a fix: opensearch vector db query structures, result mapping, filters, bulk query actions, knn_vector usage 2025-03-06 23:49:54 +01:00
wangtunan 7d1a094f9b fix: stopTask func need use async/await syntax 2025-03-07 00:07:04 +08:00
wangtunan 533c99e384 fix: stopTask func need use async/await syntax 2025-03-06 23:56:41 +08:00
orenzhang 92fb1109b6 i18n(common): add i18n translation 2025-03-06 20:16:34 +08:00
Fabio Polito 98857184ff Merge remote-tracking branch 'upstream/dev' into docling_context_extraction_engine
merge with dev branch
2025-03-06 12:12:50 +00:00
Panda 98376fbbce i18n: zh-cn 2025-03-06 13:08:35 +01:00
Panda 561f4d5d69 i18n: zh-cn 2025-03-06 11:26:04 +01:00
Perry Li 9cc9df3018 fix(chat): resolve duplicate collapsible IDs causing citation modal failures
Fix an issue where clicking inline citations in subsequent chat messages failed to open the citation modal when multiple collapsible sections are present.

The root cause was duplicate "collapsible-sources" IDs assigned to all Collapsible components. This led document.getElementById() to always return the first instance, preventing subsequent messages from opening their CitationModal.

Changes:
- Modify Collapsible ID generation in Citations.svelte to use unique IDs with "collapsible-${message.id}" pattern
- Update ResponseMessage.svelte's onSourceClick handler to reference the dynamic collapsible IDs
- Ensure proper citation modal binding for each chat message's sources

Affected components:
- Collapsible (expandable content sections)
- CitationsModal (citation detail popup)

This ensures each chat message's sources are independently collapsible and maintains proper citation modal binding throughout message history.
2025-03-06 10:10:53 +00:00
Marko Henning 41a4cf7106 Added new k_reranker parameter 2025-03-06 10:47:57 +01:00
Timothy Jaeryang Baek 3b70cd64d7 Merge pull request #11257 from open-webui/dev
0.5.20
2025-03-05 19:44:45 -08:00
Timothy Jaeryang Baek 1173459eee doc: changelog 2025-03-05 19:42:41 -08:00
Timothy Jaeryang Baek c0ecff9d70 fix: pinyin keyboard 2025-03-05 19:36:30 -08:00
Timothy Jaeryang Baek d4fca9dabf chore: format 2025-03-05 19:17:41 -08:00
Timothy Jaeryang Baek aaaebfabbe chore: format 2025-03-05 19:16:59 -08:00
Timothy Jaeryang Baek 6eccf8668e fix: non-root container issue 2025-03-05 19:14:43 -08:00
Timothy Jaeryang Baekandrecrudesce 5e78a5c510 enh: enable_code_execution toggle
Co-Authored-By: recrudesce <6450799+recrudesce@users.noreply.github.com>
2025-03-05 19:06:28 -08:00
Timothy Jaeryang Baek dce24225e0 Merge pull request #11249 from TiancongLx/dev
i18n: update zh-TW
2025-03-05 18:52:34 -08:00
Timothy Jaeryang Baek f9b44c6bf2 refac 2025-03-05 18:39:32 -08:00
Timothy Jaeryang Baek f573a90a56 Merge pull request #11254 from open-webui/main
dev
2025-03-05 18:11:18 -08:00
Timothy Jaeryang Baek 23bb0d927f chore: format 2025-03-05 18:10:24 -08:00
Fabio Polito 2982893d0d fix: format fixes 2025-03-06 00:39:00 +00:00
Tiancong Li 6b0af942db i18n: update zh-TW 2025-03-06 07:23:55 +08:00
Fabio Polito 0716f96da8 style: change style in DoclingLoader 2025-03-05 23:15:55 +00:00
Timothy Jaeryang Baek f95a482c24 Merge pull request #11209 from aleixdorca/dev
Update catalan translation.json
2025-03-05 14:56:37 -08:00
Timothy Jaeryang Baek edf94d732b refac 2025-03-05 14:54:48 -08:00
Fabio Polito 9aa407dbd2 feat: merge with main 2025-03-05 22:04:34 +00:00
Timothy Jaeryang Baek 73a77abd20 Merge pull request #11245 from tupe2009/kleqon.fix-es-bugs
fix: ElasticSearch indexes delete and upsert bugs(Post 0.5.19 release)
2025-03-05 13:36:55 -08:00
ofek a8f205213c fixed es bugs 2025-03-05 23:19:56 +02:00
Fabio Polito a44b35e99e fix: fix DoclingLoader input params 2025-03-05 17:53:45 +00:00
Timothy Jaeryang Baek 4770285c04 Merge pull request #11211 from open-webui/dev
fix: site.webmanifest
2025-03-05 00:45:21 -08:00
Timothy Jaeryang Baek 1639fbb544 fix: site.webmanifest 2025-03-05 00:45:04 -08:00
Aleix Dorca 2dc5f3c30c Update catalan translation.json 2025-03-05 08:42:16 +01:00
Timothy Jaeryang Baek 1a51584fe0 Merge pull request #10939 from open-webui/dev
0.5.19
2025-03-04 22:22:20 -08:00
Timothy Jaeryang Baek 976c94ba5a chore: pyproject 2025-03-04 22:13:51 -08:00
Timothy Jaeryang Baek 4df74690e1 doc: changelog 2025-03-04 22:10:51 -08:00
Timothy Jaeryang Baek e2d60bd92e chore: format 2025-03-04 22:10:47 -08:00
Timothy Jaeryang Baek c2eada36f1 Merge pull request #10898 from hurxxxx/fix/update-msal-browser-version
fix: update msal browser version
2025-03-04 21:56:22 -08:00
Timothy Jaeryang Baek 0ce6975ec8 chore: bump 2025-03-04 21:49:42 -08:00
Timothy Jaeryang Baek 1ddc56fcf8 Merge pull request #11201 from open-webui/main
dev
2025-03-04 21:43:33 -08:00
Timothy Jaeryang Baek 7f7365db4f Merge pull request #11132 from dongs0104/dev
fix: email claim constant
2025-03-04 21:39:28 -08:00
Timothy Jaeryang Baek c9e2ba56c6 Merge pull request #11198 from xinhai-ai/refa_filters
chore: move the filter_type check earlier to avoid unnecessary valve reads.
2025-03-04 21:27:36 -08:00
Timothy Jaeryang Baek b01c9184c2 chore: format 2025-03-04 21:24:17 -08:00
星海 3ee1ae8c95 refac: filters 2025-03-05 13:05:14 +08:00
星海 8ce6c8cf78 refac: filters 2025-03-05 13:00:45 +08:00
Timothy Jaeryang Baek fbf4f5eb30 refac 2025-03-04 18:19:05 -08:00
Timothy Jaeryang Baek 6c77e6fdb3 refac 2025-03-04 18:08:43 -08:00
Timothy Jaeryang Baek 3b74431ea3 refac: filters 2025-03-04 18:04:55 -08:00
Timothy Jaeryang Baek 53fe7c460b Merge pull request #11178 from silentoplayz/fix
chore: Update ToolkitEditor.svelte
2025-03-04 17:53:47 -08:00
Timothy Jaeryang Baek f39556b933 Merge pull request #11163 from tidely/pathfix
chore: Use platform specific path separators
2025-03-04 17:53:21 -08:00
Timothy Jaeryang Baek cffd02defe Merge pull request #11153 from panda44312/patch-4
i18n: update ZH-CN
2025-03-04 17:42:05 -08:00
Timothy Jaeryang Baek bdc6088159 Merge pull request #11151 from TiancongLx/dev
i18n: update zh-TW
2025-03-04 17:41:57 -08:00
Timothy Jaeryang Baek 2a75a85556 refac 2025-03-04 17:40:32 -08:00
Silentoplayz 94c025c6d7 Update ToolkitEditor.svelte
fix
2025-03-04 17:46:24 -05:00
Timothy Jaeryang Baek 8961bc2453 Merge pull request #11173 from TFWol/patch-2
chore: Revamp Feature Request template
2025-03-04 13:49:07 -08:00
TFWol cb82c7062a Revamp Feature Request template 2025-03-04 11:18:50 -08:00
tidely b15814c42f chore: remove unnecessary Path conversions
Remove unnecessary `pathlib.Path` conversions. (CACHE_DIR and DATA_DIR)

Use `/` Path joining shorthand to ensure using platform specific Path separators (Windows: \\, Unix: /)
2025-03-04 19:53:52 +02:00
Panda 023c8bb5b4 i18n: zh-cn Update 2025-03-04 16:08:06 +01:00
Tiancong Li 500440ebec i18n: update zh-TW 2025-03-04 22:51:24 +08:00
Timothy Jaeryang Baek b94de4f002 refac 2025-03-04 02:45:05 -08:00
Timothy Jaeryang Baek 039a1e1f14 refac: assets 2025-03-04 01:47:17 -08:00
Timothy Jaeryang Baek 14a6c1f496 refac: favicon 2025-03-04 01:30:22 -08:00
Timothy Jaeryang Baek 0a295ff56d refac: icons 2025-03-04 01:25:47 -08:00
Timothy Jaeryang Baek 36ffa9824b refac 2025-03-04 01:16:25 -08:00
Timothy Jaeryang Baek e3769c1073 refac 2025-03-04 01:16:08 -08:00
Timothy Jaeryang Baek d78fe4decc Merge pull request #11073 from vincent-thevenin/dev
fix: fix issue with call voice recording starting before voice detection
2025-03-04 00:55:03 -08:00
Timothy Jaeryang Baek 7b442e4be0 Merge pull request #11141 from Youggls/dev
fix: correct parameter name for MilvusClient instantiation
2025-03-04 00:54:49 -08:00
Timothy Jaeryang Baek d17ac95f95 Merge pull request #11143 from panda44312/patch-3
i18n: zh-cn
2025-03-04 00:53:31 -08:00
Panda 59c3203990 IUpdate translation.json 2025-03-04 09:48:03 +01:00
Timothy Jaeryang Baek a4747c88e0 refac 2025-03-04 00:33:19 -08:00
Timothy Jaeryang Baek 39ea59edc8 chore: format 2025-03-04 00:32:27 -08:00
Perry Li 67ed61d022 fixbug: correct parameter name for MilvusClient instantiation
Replace incorrect parameter 'database=MILVUS_DB' with valid 'db_name=MILVUS_DB'
2025-03-04 16:02:19 +08:00
Dong Shin b8f3abda5a fix: email claim constant 2025-03-04 15:50:04 +09:00
Timothy Jaeryang Baek 8697f72068 refac: styling 2025-03-03 22:44:03 -08:00
Timothy Jaeryang Baek 3e7077bb73 refac 2025-03-03 22:24:19 -08:00
Timothy Jaeryang Baek 5f89c1b137 enh: model selector tag filter 2025-03-03 21:47:27 -08:00
Timothy Jaeryang Baek 811e1f92b1 refac 2025-03-03 21:27:48 -08:00
Timothy Jaeryang Baek 15cbccc8f5 enh: code block collapse 2025-03-03 21:10:15 -08:00
Timothy Jaeryang Baek 1cfa491179 Merge pull request #10868 from tidely/loggingnamemap
chore: use logging.getLevelNamesMapping() for validating log level
2025-03-03 20:55:22 -08:00
Timothy Jaeryang Baek 35e7e07821 refac: cuda error gc 2025-03-03 20:55:07 -08:00
Timothy Jaeryang Baek 9ff54288ef refac 2025-03-03 20:53:14 -08:00
Timothy Jaeryang Baek baca6db680 Merge pull request #10987 from alpha-pet/feat-submit-on-ctrl-enter
feat: add alternative input configuration where Ctrl-Enter is required to send chat messages
2025-03-03 20:39:53 -08:00
Timothy Jaeryang Baek 6bd2bc5683 refac 2025-03-03 20:34:45 -08:00
Timothy Jaeryang Baek 029625ec4e refac 2025-03-03 20:34:17 -08:00
Timothy Jaeryang Baek 7e65f7fa4d fix: chat event handler not being registered on sign in 2025-03-03 20:31:35 -08:00
Timothy Jaeryang Baek 80f5573cf3 refac: AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST 2025-03-03 20:24:10 -08:00
Timothy Jaeryang Baek 1236157800 fix: timestamp missing in edited user message 2025-03-03 20:21:39 -08:00
Timothy Jaeryang Baek e628bfe6ff fix 2025-03-03 20:16:40 -08:00
Timothy Jaeryang Baek bb2bd7d721 Merge pull request #11120 from OrenZhang/fix_jupyter
fix(jupyter): fix kernel_id not set and optimize code
2025-03-03 20:15:09 -08:00
Timothy Jaeryang Baek 9c107f9d2d refac: images "url" fallback 2025-03-03 20:07:59 -08:00
orenzhang fafd847f18 refactor(jupyter): change log message for execution failed 2025-03-04 12:01:08 +08:00
orenzhang 3a7e5a5f31 feat(jupyter): check response status for destroy kernel 2025-03-04 12:00:04 +08:00
orenzhang 613d278afc refactor(jupyter): change log level 2025-03-04 11:58:32 +08:00
orenzhang 96e0c4d7b8 refactor(jupyter): replace httpx with aiohttp 2025-03-04 11:56:11 +08:00
Timothy Jaeryang Baek 46cea1f904 fix: ldap email case sensitive 2025-03-03 19:52:27 -08:00
Timothy Jaeryang Baek d844fc7edb refac: inline citation handling 2025-03-03 19:48:00 -08:00
Timothy Jaeryang Baek 3468dda556 refac: rm unused api endpoint 2025-03-03 19:33:55 -08:00
Timothy Jaeryang Baek e6ff4169e1 Merge pull request #11066 from NibbinNone/dev
feat: add support for reasoning_content
2025-03-03 16:37:28 -08:00
Timothy Jaeryang Baek 6f8c1a8f0d Merge pull request #11089 from tupe2009/kleqon.feat-add-elasticsearch-support
feat: Elasticsearch as a vector store support
2025-03-03 16:34:39 -08:00
Timothy Jaeryang Baek d529be86fe Merge pull request #11109 from panda44312/patch-2
i18n: Update zh-cn
2025-03-03 16:34:21 -08:00
Panda 3ad4b10470 i18n: Update zh-cn 2025-03-04 00:17:02 +01:00
Timothy Jaeryang Baek 376fce273e Merge pull request #11079 from MeteorSkyOne/dev
fix: allow error returns None
2025-03-03 14:57:54 -08:00
Timothy Jaeryang Baek c19960002b Merge pull request #11086 from aindriu80/main
(i18n)Updated Irish translations
2025-03-03 14:57:01 -08:00
Timothy Jaeryang Baek dc2b8fd327 Merge branch 'dev' into main 2025-03-03 14:56:48 -08:00
ofek 737dfd2763 added elasticsearch support 2025-03-03 23:39:42 +02:00
tarmst 1ad80490de Add read/write access control for files from knowledge 2025-03-03 20:03:21 +00:00
Aindriú Mac Giolla Eoin 3fa323d1a1 Updated Irish translations 2025-03-03 18:41:11 +00:00
MeteorSky a01ef6cfa0 fix: allow error returns None 2025-03-04 00:08:49 +08:00
Timothy Jaeryang Baek c7d4d2e41f Merge pull request #11067 from TiancongLx/dev
i18n: update zh-TW (fix typo)
2025-03-03 06:49:27 -08:00
orenzhang 744ffbb1fb fix(jupyter): fix kernel_id not set and optimize code 2025-03-03 22:18:02 +08:00
vincent-thevenin 4d6132b9de fix: fix issue with call voice recording starting before voice detection 2025-03-03 13:59:28 +01:00
Tiancong Li 7a31ff6a2f i18n: update zh-TW (fix typo) 2025-03-03 16:55:37 +08:00
CorbinChen 516f1cf67b feat: add support for reasoning_content 2025-03-03 16:47:35 +08:00
Timothy Jaeryang Baek 3d6e48b05e Merge pull request #11043 from TiancongLx/dev
i18n: update zh-TW
2025-03-02 12:16:56 -08:00
Tiancong Li 3dd492627e i18n: update zh-TW 2025-03-02 17:04:58 +08:00
Timothy Jaeryang Baek 66210b95fc Merge pull request #11038 from satlus/pwa-fix-linkrel
fixed manifest in app.html restoring PWA
2025-03-01 19:55:08 -08:00
satlus 6e58d99920 fixed manifest link rel, restoring PWA 2025-03-01 19:43:40 -05:00
Timothy Jaeryang Baek 6471f12668 Merge pull request #11033 from dtaivpp/main
fix: Changed to use collection_name and fixed bulk indexing missing index.
2025-03-01 16:00:13 -08:00
David Tippett f3c4c2b8e3 Changed to use colleciton name and fixed bulk indexing missing index. 2025-03-01 13:26:19 -05:00
Timothy Jaeryang Baek a73b6448c5 Merge pull request #11032 from cheahjs/chore/build-arm-on-arm
ci: Update GitHub Actions runner selection for multi-platform builds
2025-03-01 08:23:54 -08:00
Jun Siang Cheah 3238981202 ci: Update GitHub Actions runner selection for multi-platform builds 2025-03-01 16:04:26 +00:00
Timothy Jaeryang Baek 59ddce1ea3 chore: bump 2025-03-01 08:00:40 -08:00
Timothy Jaeryang Baek 2b025b267a chore: bump 2025-03-01 07:57:08 -08:00
Timothy Jaeryang Baek efe8c4ca69 chore: format 2025-03-01 07:28:00 -08:00
Timothy Jaeryang Baek d7088efe73 fix: "stream" hook not working 2025-03-01 06:56:24 -08:00
Timothy Jaeryang Baek 05c5e73304 Merge pull request #10373 from dannyl1u/logit_bias
feat: logit bias
2025-03-01 06:13:19 -08:00
Timothy Jaeryang Baek f00f518abe Merge pull request #11011 from open-webui/dependabot/pip/backend/dev/sqlalchemy-2.0.38
chore(deps): bump sqlalchemy from 2.0.32 to 2.0.38 in /backend
2025-02-28 18:44:20 -08:00
Timothy Jaeryang Baek 639f4a44e0 Merge pull request #11013 from open-webui/dependabot/pip/backend/dev/black-25.1.0
chore(deps): bump black from 24.8.0 to 25.1.0 in /backend
2025-02-28 18:44:14 -08:00
Timothy Jaeryang Baek 92e1c654ce Merge pull request #11014 from open-webui/dependabot/pip/backend/dev/peewee-3.17.9
chore(deps): bump peewee from 3.17.8 to 3.17.9 in /backend
2025-02-28 18:44:07 -08:00
Timothy Jaeryang Baek 81d2df4df9 Merge pull request #11015 from open-webui/dependabot/pip/backend/dev/bcrypt-4.3.0
chore(deps): bump bcrypt from 4.2.0 to 4.3.0 in /backend
2025-02-28 18:43:57 -08:00
Timothy Jaeryang Baek 65949dbcbc Merge pull request #11012 from open-webui/dependabot/pip/backend/dev/uvicorn-standard--0.34.0
chore(deps): bump uvicorn[standard] from 0.30.6 to 0.34.0 in /backend
2025-02-28 18:43:47 -08:00
dependabot[bot] eb97e30b0a chore(deps): bump bcrypt from 4.2.0 to 4.3.0 in /backend
Bumps [bcrypt](https://github.com/pyca/bcrypt) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/pyca/bcrypt/blob/main/release.py)
- [Commits](https://github.com/pyca/bcrypt/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:41 +00:00
dependabot[bot] 84518490c5 chore(deps): bump peewee from 3.17.8 to 3.17.9 in /backend
Bumps [peewee](https://github.com/coleifer/peewee) from 3.17.8 to 3.17.9.
- [Release notes](https://github.com/coleifer/peewee/releases)
- [Changelog](https://github.com/coleifer/peewee/blob/master/CHANGELOG.md)
- [Commits](https://github.com/coleifer/peewee/compare/3.17.8...3.17.9)

---
updated-dependencies:
- dependency-name: peewee
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:36 +00:00
dependabot[bot] dd73ecbdc9 chore(deps): bump black from 24.8.0 to 25.1.0 in /backend
Bumps [black](https://github.com/psf/black) from 24.8.0 to 25.1.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.8.0...25.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:32 +00:00
dependabot[bot] 588a179b7f chore(deps): bump uvicorn[standard] from 0.30.6 to 0.34.0 in /backend
Bumps [uvicorn[standard]](https://github.com/encode/uvicorn) from 0.30.6 to 0.34.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.30.6...0.34.0)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:27 +00:00
dependabot[bot] 244915cd1e chore(deps): bump sqlalchemy from 2.0.32 to 2.0.38 in /backend
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.32 to 2.0.38.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: sqlalchemy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:24 +00:00
Timothy Jaeryang Baek 094d777e55 Merge pull request #11009 from amuwall/fix-typing-error
fix: typing error in replace_messages_variable func args
2025-02-28 18:34:24 -08:00
amuwall b430deab15 fix: fix typing error in args 2025-03-01 10:20:29 +08:00
Timothy Jaeryang Baek fe44e4d344 i18n(html): sync html language with i18n language 2025-02-28 16:06:28 -08:00
Timothy Jaeryang Baek 88c02d5a14 refac 2025-02-28 16:02:15 -08:00
Timothy Jaeryang Baek b75b891de1 Merge pull request #10983 from NovoNordisk-OpenSource/yvedeng/fix-redis-timeout-config
fix: add redis lock timeout config
2025-02-28 16:01:59 -08:00
Timothy Jaeryang Baek f1ce653830 Merge pull request #10992 from softue/i18n-floating-buttons
i18n floating buttons (en-US and pt-BR)
2025-02-28 15:59:49 -08:00
Rômulo Mendes Figueiredo c06484297b i18n: ask and explain floating buttons 2025-02-28 12:39:57 -03:00
Timothy Jaeryang Baek 331caad5b8 fix 2025-02-28 07:36:56 -08:00
Thomas Rehn 8b3fcfea13 feat: add alternative input configuration where Ctrl-Enter is required to send chat messages 2025-02-28 15:23:03 +01:00
Timothy Jaeryang Baek 05599e1bb5 Merge pull request #10967 from OrenZhang/i18n_zh_cn
i18n(zh-cn): add chinese translation
2025-02-28 03:21:51 -08:00
orenzhang bcdbc4fb8a i18n(zh-cn): add chinese translation 2025-02-28 17:47:27 +08:00
Yifang Deng 41a2b86e70 fix: add redis lock timeout config 2025-02-28 10:12:51 +01:00
Danny Liu f9292ab4d2 Merge branch 'dev' into logit_bias 2025-02-27 23:51:13 -08:00
dannyl1u f4bd094128 Merge remote-tracking branch 'origin' into logit_bias 2025-02-27 23:48:22 -08:00
dannyl1u 90aa29528c logit_bias: handle comma seperated values 2025-02-27 23:13:30 -08:00
dannyl1u 34e3cb6881 logit bias: update tooltip message 2025-02-27 23:13:09 -08:00
Timothy Jaeryang Baek d0ddb0637e enh: web embed bypass embedding and retrieval support 2025-02-27 16:34:05 -08:00
hurxxxx 2405809f92 Merge branch 'main' into fix/update-msal-browser-version 2025-02-28 08:44:06 +09:00
hurxxxx 77579f5509 chore: cleanup 2025-02-28 08:42:02 +09:00
Timothy Jaeryang Baek 5e33955566 Merge pull request #10940 from TheMasterFX/update-de-DE-translation
i18n: Update de-DE translation
2025-02-27 15:17:59 -08:00
Timothy Jaeryang Baek 73c0262345 Merge branch 'dev' into update-de-DE-translation 2025-02-27 15:17:51 -08:00
Timothy Jaeryang Baek a3ea924f0a Merge pull request #10941 from TiancongLx/dev
i18n: update zh-TW
2025-02-27 15:17:03 -08:00
Tiancong Li 01435997b9 i18n: update zh-TW 2025-02-28 06:29:53 +08:00
Timmo 90fe437e29 i18n: Update de-DE translation - fix typos 2025-02-27 23:23:05 +01:00
Timmo 43077b82b7 i18n: Update de-DE translation 2025-02-27 23:09:02 +01:00
Timothy Jaeryang Baek 1b56a8f3cb Merge pull request #10864 from kurtdami/perplexity_integration
feat: add perplexity integration to web search
2025-02-27 13:51:03 -08:00
Timothy Jaeryang Baek 1d27402dd0 Merge pull request #10910 from saraangelmurphy/awsworkloadidentity
feat: add AWS workload identity support
2025-02-27 13:50:36 -08:00
Timothy Jaeryang Baek 6f7161acf8 chore: format 2025-02-27 13:04:39 -08:00
Timothy Jaeryang Baek 54e3ca8e6c refac: rm confusing defaults 2025-02-27 13:03:28 -08:00
Sara Angel-Murphy 5e873bc643 feat: add AWS workload identity support 2025-02-27 13:12:54 -05:00
hurxxxx c5cfcc1229 chore: cleanup 2025-02-27 23:16:52 +09:00
hurxxxx 203dfdedf1 Update deprecated msal-browser version to the latest 2025-02-27 23:12:09 +09:00
hurxxxx febc9ab885 Merge remote-tracking branch 'upstream/main' 2025-02-27 21:08:20 +09:00
tidely d7b18b662b chore: use logging.getLevelNamesMapping()
Use `logging.getLevelNamesMapping()` for getting all valid logging levels. This also allows adding new ones using `logging.addLevel()`. This feature was [added](https://docs.python.org/3.11/library/logging.html#logging.getLevelNamesMapping) in Python 3.11 which is above the minimum supported Python version for open-webui.
2025-02-27 11:19:27 +02:00
kurtdami b061775932 feat: add perplexity integration to web search 2025-02-27 00:30:48 -08:00
hurxxxx f865a23a18 Merge remote-tracking branch 'upstream/main' 2025-02-20 23:08:47 +09:00
dannyl1u 8aa6415500 include logit_bias in form_data 2025-02-19 16:23:58 -08:00
dannyl1u 4a2a12fd21 feat: scaffolding for logit_bias 2025-02-19 10:33:49 -08:00
hurxxxx a381ffc585 revert 2025-02-18 23:17:06 +09:00
hurxxxx cdc13e9960 chore: Fix unnecessary whitespace issues 2025-02-18 23:15:57 +09:00
Fabio Polito 2419ef06a0 feat: docling support for document preprocessing 2025-02-14 12:08:03 +00:00
JT 40dea3fbe1 Merge branch 'dev' into main 2025-02-05 15:15:24 -08:00
jayteaftw 157c781b0a Merge branch 'main' of https://github.com/jayteaftw/open-webui 2025-02-05 14:07:59 -08:00
jayteaftw 6d2f87e904 Added server side Prefixing 2025-02-05 14:03:16 -08:00
JT 81102f4be2 Merge branch 'open-webui:main' into main 2025-02-04 13:06:04 -08:00
jvinolus 7b8e5d4e7c Fixed errors and added more support 2025-02-04 13:04:36 -08:00
Matteo Sirri 22c100bb6b feat: add contributing guide 2025-02-03 16:45:25 +00:00
Matteo Sirri e6715ce8b8 docs: fix readme 2025-02-03 16:43:59 +00:00
Matteo Sirri 65443a3a66 feat: initial commit 2025-02-03 16:35:46 +00:00
jvinolus 47b8412695 Initialize support for prefixing embeddings 2025-01-15 17:05:04 -08:00
357 changed files with 40525 additions and 13226 deletions
-80
View File
@@ -1,80 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
# Bug Report
## Important Notes
- **Before submitting a bug report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. This will help us efficiently focus on improving the project.
- **Collaborate respectfully**: We value a constructive attitude, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We’re here to help if you’re open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. We appreciate your time and ask that you respect ours.
- **Contributing**: If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
---
## Installation Method
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
## Environment
- **Open WebUI Version:** [e.g., v0.3.11]
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
**Confirmation:**
- [ ] I have read and followed all the instructions provided in the README.md.
- [ ] I am on the latest version of both Open WebUI and Ollama.
- [ ] I have included the browser console logs.
- [ ] I have included the Docker container logs.
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
## Expected Behavior:
[Describe what you expected to happen.]
## Actual Behavior:
[Describe what actually happened.]
## Description
**Bug Summary:**
[Provide a brief but clear summary of the bug]
## Reproduction Details
**Steps to Reproduce:**
[Outline the steps to reproduce the bug. Be as detailed as possible.]
## Logs and Screenshots
**Browser Console Logs:**
[Include relevant browser console logs, if applicable]
**Docker Container Logs:**
[Include relevant Docker container logs, if applicable]
**Screenshots/Screen Recordings (if applicable):**
[Attach any relevant screenshots to help illustrate the issue]
## Additional Information
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
## Note
If the bug report is incomplete or does not follow the provided instructions, it may not be addressed. Please ensure that you have followed the steps outlined in the README.md and troubleshooting.md documents, and provide all necessary information for us to reproduce and address the issue. Thank you!
+146
View File
@@ -0,0 +1,146 @@
name: Bug Report
description: Create a detailed bug report to help us improve Open WebUI.
title: 'issue: '
labels: ['bug', 'triage']
assignees: []
body:
- type: markdown
attributes:
value: |
# Bug Report
## Important Notes
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project.
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
- **Contributing**: If you encounter an issue, consider submitting a pull request or forking the project. We prioritize preventing contributor burnout to maintain Open WebUI's quality.
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
- type: checkboxes
id: issue-check
attributes:
label: Check Existing Issues
description: Confirm that you’ve checked for existing reports before submitting a new one.
options:
- label: I have searched the existing issues and discussions.
required: true
- label: I am using the latest version of Open WebUI.
required: true
- type: dropdown
id: installation-method
attributes:
label: Installation Method
description: How did you install Open WebUI?
options:
- Git Clone
- Pip Install
- Docker
- Other
validations:
required: true
- type: input
id: open-webui-version
attributes:
label: Open WebUI Version
description: Specify the version (e.g., v0.3.11)
validations:
required: true
- type: input
id: ollama-version
attributes:
label: Ollama Version (if applicable)
description: Specify the version (e.g., v0.2.0, or v0.1.32-rc1)
validations:
required: false
- type: input
id: operating-system
attributes:
label: Operating System
description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04)
validations:
required: true
- type: input
id: browser
attributes:
label: Browser (if applicable)
description: Specify the browser/version (e.g., Chrome 100.0, Firefox 98.0)
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: Confirmation
description: Ensure the following prerequisites have been met.
options:
- label: I have read and followed all instructions in `README.md`.
required: true
- label: I am using the latest version of **both** Open WebUI and Ollama.
required: true
- label: I have included the browser console logs.
required: true
- label: I have included the Docker container logs.
required: true
- label: I have listed steps to reproduce the bug in detail.
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: Describe what should have happened.
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual Behavior
description: Describe what actually happened.
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Steps to Reproduce
description: Providing clear, step-by-step instructions helps us reproduce and fix the issue faster. If we can't reproduce it, we can't fix it.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See the error message '...'
validations:
required: true
- type: textarea
id: logs-screenshots
attributes:
label: Logs & Screenshots
description: Include relevant logs, errors, or screenshots to help diagnose the issue.
placeholder: 'Attach logs from the browser console, Docker logs, or error messages.'
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Provide any extra details that may assist in understanding the issue.
validations:
required: false
- type: markdown
attributes:
value: |
## Note
If the bug report is incomplete or does not follow instructions, it may not be addressed. Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
Thank you for contributing to Open WebUI!
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
-35
View File
@@ -1,35 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
# Feature Request
## Important Notes
- **Before submitting a report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. This will help us efficiently focus on improving the project.
- **Collaborate respectfully**: We value a constructive attitude, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We’re here to help if you’re open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. We appreciate your time and ask that you respect ours.
- **Contributing**: If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,64 @@
name: Feature Request
description: Suggest an idea for this project
title: 'feat: '
labels: ['triage']
body:
- type: markdown
attributes:
value: |
## Important Notes
### Before submitting
Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
It's likely we're already tracking it! If you’re unsure, start a discussion post first.
This will help us efficiently focus on improving the project.
### Collaborate respectfully
We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**.
Remember:
- Open WebUI is a **volunteer-driven project**
- It's managed by a **single maintainer**
- It's supported by contributors who also have **full-time jobs**
We appreciate your time and ask that you **respect ours**.
### Contributing
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
### Bug reproducibility
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
- type: checkboxes
id: existing-issue
attributes:
label: Check Existing Issues
description: Please confirm that you've checked for existing similar requests
options:
- label: I have searched the existing issues and discussions.
required: true
- type: textarea
id: problem-description
attributes:
label: Problem Description
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
placeholder: "Ex. I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution-description
attributes:
label: Desired Solution you'd like
description: Clearly describe what you want to happen.
validations:
required: true
- type: textarea
id: alternatives-considered
attributes:
label: Alternatives Considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
+14
View File
@@ -1,12 +1,26 @@
version: 2
updates:
- package-ecosystem: uv
directory: '/'
schedule:
interval: monthly
target-branch: 'dev'
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: monthly
target-branch: 'dev'
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
target-branch: 'dev'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: monthly
target-branch: 'dev'
+7 -3
View File
@@ -9,9 +9,9 @@
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
- [ ] **Testing:** Have you written and run sufficient tests to validate the changes?
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
- [ ] **Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows
@@ -22,7 +22,7 @@
- **i18n**: Internationalization or localization changes
- **perf**: Performance improvement
- **refactor**: Code restructuring for better maintainability, readability, or scalability
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
- **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.)
- **test**: Adding missing tests or correcting existing tests
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
@@ -70,3 +70,7 @@
### Screenshots or Videos
- [Attach any relevant screenshots or videos demonstrating the changes]
### Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
+3 -3
View File
@@ -14,7 +14,7 @@ env:
jobs:
build-main-image:
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
@@ -111,7 +111,7 @@ jobs:
retention-days: 1
build-cuda-image:
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
@@ -211,7 +211,7 @@ jobs:
retention-days: 1
build-ollama-image:
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
+12 -2
View File
@@ -5,10 +5,18 @@ on:
branches:
- main
- dev
paths:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
branches:
- main
- dev
paths:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
jobs:
build:
@@ -17,7 +25,9 @@ jobs:
strategy:
matrix:
python-version: [3.11]
python-version:
- 3.11.x
- 3.12.x
steps:
- uses: actions/checkout@v4
@@ -25,7 +35,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: '${{ matrix.python-version }}'
- name: Install dependencies
run: |
+9 -1
View File
@@ -5,10 +5,18 @@ on:
branches:
- main
- dev
paths-ignore:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
branches:
- main
- dev
paths-ignore:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
jobs:
build:
@@ -21,7 +29,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22' # Or specify any other version you want to use
node-version: '22'
- name: Install Dependencies
run: npm install
+294
View File
@@ -5,6 +5,300 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.9] - 2025-05-10
### Added
- 📝 **Edit Attached Images/Files in Messages**: You can now easily edit your sent messages by removing attached files—streamlining document management, correcting mistakes on the fly, and keeping your chats clutter-free.
- 🚨 **Clear Alerts for Private Task Models**: When interacting with private task models, the UI now clearly alerts you—making it easier to understand resource availability and access, reducing confusion during workflow setup.
### Fixed
- 🛡️ **Confirm Dialog Focus Trap Reliability**: The focus now stays correctly within confirmation dialogs, ensuring keyboard navigation and accessibility is seamless and preventing accidental operations—especially helpful during critical or rapid workflows.
- 💬 **Temporary Chat Admin Controls & Session Cleanliness**: Admins are now able to properly enable temporary chat mode without errors, and previous session prompts or tool selections no longer carry over—delivering a fresh, predictable, and consistent temporary chat experience every time.
- 🤖 **External Reranker Integration Functionality Restored**: External reranker integrations now work correctly, allowing you to fully leverage advanced ranking services for sharper, more relevant search results in your RAG and knowledge base workflows.
## [0.6.8] - 2025-05-10
### Added
- 🏆 **External Reranker Support for Knowledge Base Search**: Supercharge your Retrieval-Augmented Generation (RAG) workflows with the new External Reranker integration; easily plug in advanced reranking services via the UI to deliver sharper and more relevant search results, accelerating research and insight discovery.
- 📤 **Unstylized PDF Export Option (Reduced File Size)**: When exporting chat transcripts or documents, you can now choose an unstylized PDF export for snappier downloads, minimal file size, and clean data archiving—perfect for large-scale storage or sharing.
- 📝 **Vazirmatn Font for Persian & Arabic**: Arabic and Persian users will now see their text beautifully rendered with the specialized Vazirmatn font for an improved localized reading experience.
- 🏷️ **SharePoint Tenant ID Support for OneDrive**: You can now specify a SharePoint tenant ID in OneDrive settings for seamless authentication and granular enterprise integration.
- 👤 **Refresh OAuth Profile Picture**: Your OAuth profile picture now updates in real-time, ensuring your presence and avatar always match your latest identity across integrated platforms.
- 🔧 **Milvus Configuration Improvements**: Configure index and metric types for Milvus directly within settings; take full control of your vector database for more accurate and robust AI search experiences.
- 🛡️ **S3 Tagging Toggle for Compatibility**: Optional S3 tagging via an environment toggle grants full compatibility with all storage backends—including those that don’t support tagging like Cloudflare R2—ensuring error-free attachment and document management.
- 👨‍🦯 **Icon Button Accessibility Improvements**: Key interactive icon-buttons now include aria-labels and ARIA descriptions, so screen readers provide precise guidance about what action each button performs for improved accessibility.
- ♿ **Enhanced Accessibility with Modal Focus Trap**: Modal dialogs and pop-ups now feature a focus trap and improved ARIA roles, ensuring seamless navigation and screen reader support—making the interface friendlier for everyone, including keyboard and assistive tech users.
- 🏃 **Improved Admin User List Loading Indicator**: The user list loading experience is now clearer and more responsive in the admin panel.
- 🧑‍🤝‍🧑 **Larger Admin User List Page Size**: Admins can now manage up to 30 users per page in the admin interface, drastically reducing pagination and making large user teams easier and faster to manage.
- 🌠 **Default Code Interpreter Prompt Clarified**: The built-in code interpreter prompt is now more explicit, preventing AI from wrapping code in Markdown blocks when not needed—ensuring properly formatted code runs as intended every time.
- 🧾 **Improved Default Title Generation Prompt Template**: Title generation now uses a robust template for reliable JSON output, improving chat organization and searchability.
- 🔗 **Support Jupyter Notebooks with Non-Root Base URLs**: Notebook-based code execution now supports non-root deployed Jupyter servers, granting full flexibility for hybrid or multi-user setups.
- 📰 **UI Scrollbar Always Visible for Overflow Tools**: When available tools overflow the display, the scrollbar is now always visible and there’s a handy "show all" toggle, making navigation of large toolsets snappier and more intuitive.
- 🛠️ **General Backend Refactoring for Stability**: Multiple under-the-hood improvements have been made across backend components, ensuring smoother performance, fewer errors, and a more reliable overall experience for all users.
- 🚀 **Optimized Web Search for Faster Results**: Web search speed and performance have been significantly enhanced, delivering answers and sources in record time to accelerate your research-heavy workflows.
- 💡 **More Supported Languages**: Expanded language support ensures an even wider range of users can enjoy an intuitive and natural interface in their native tongue.
### Fixed
- 🏃‍♂️ **Exhausting Workers in Nginx Reverse Proxy Due to Websocket Fix**: Websocket sessions are now fully compatible behind Nginx, eliminating worker exhaustion and restoring 24/7 reliability for real-time chats even in complex deployments.
- 🎤 **Audio Transcription Issue with OpenAI Resolved**: OpenAI-based audio transcription now handles WebM and newer formats without error, ensuring seamless voice-to-text workflows every time.
- 👉 **Message Input RTL Issue Fixed**: The chat message input now displays correctly for right-to-left languages, creating a flawless typing and reading experience for Arabic, Hebrew, and more.
- 🀄 **Katex: Proper Rendering of Chinese Characters Next to Math**: Math formulas now render perfectly even when directly adjacent to Chinese (CJK) characters, improving visual clarity for multilingual teams and cross-language documents.
- 🔂 **Duplicate Web Search URLs Eliminated**: Search results now reliably filter out URL duplicates, so your knowledge and search citations are always clean, trimmed, and easy to review.
- 📄 **Markdown Rendering Fixed in Knowledge Bases**: Markdown is now displayed correctly within knowledge bases, enabling better formatting and clarity of information-rich files.
- 🗂️ **LDAP Import/Loading Issue Resolved**: LDAP user imports process correctly, ensuring smooth onboarding and access without interruption.
- 🌎 **Pinecone Batch Operations and Async Safety**: All Pinecone operations (batch insert, upsert, delete) now run efficiently and safely in an async environment, boosting performance and preventing slowdowns in large-scale RAG jobs.
## [0.6.7] - 2025-05-07
### Added
- 🌐 **Custom Azure TTS API URL Support Added**: You can now define a custom Azure Text-to-Speech endpoint—enabling flexibility for enterprise deployments and regional compliance.
- ⚙️ **TOOL_SERVER_CONNECTIONS Environment Variable Suppor**: Easily configure and deploy tool servers via environment variables, streamlining setup and enabling faster enterprise provisioning.
- 👥 **Enhanced OAuth Group Handling as String or List**: OAuth group data can now be passed as either a list or a comma-separated string, improving compatibility with varied identity provider formats and reducing onboarding friction.
### Fixed
- 🧠 **Embedding with Ollama Proxy Endpoints Restored**: Fixed an issue where missing API config broke embedding for proxied Ollama models—ensuring consistent performance and compatibility.
- 🔐 **OIDC OAuth Login Issue Resolved**: Users can once again sign in seamlessly using OpenID Connect-based OAuth, eliminating login interruptions and improving reliability.
- 📝 **Notes Feature Access Fixed for Non-Admins**: Fixed an issue preventing non-admin users from accessing the Notes feature, restoring full cross-role collaboration capabilities.
- 🖼️ **Tika Loader Image Extraction Problem Resolved**: Ensured TikaLoader now processes 'extract_images' parameter correctly, restoring complete file extraction functionality in document workflows.
- 🎨 **Automatic1111 Image Model Setting Applied Properly**: Fixed an issue where switching to a specific image model via the UI wasn’t reflected in generation, re-enabling full visual creativity control.
- 🏷️ **Multiple XML Tags in Messages Now Parsed Correctly**: Fixed parsing issues when messages included multiple XML-style tags, ensuring clean and unbroken rendering of rich content in chats.
- 🖌️ **OpenAI Image Generation Issues Resolved**: Resolved broken image output when using OpenAI’s image generation, ensuring fully functional visual creation workflows.
- 🔎 **Tool Server Settings UI Privacy Restored**: Prevented restricted users from accessing tool server settings via search—restoring tight permissions control and safeguarding sensitive configurations.
- 🎧 **WebM Audio Transcription Now Supported**: Fixed an issue where WebM files failed during audio transcription—these formats are now fully supported, ensuring smoother voice note workflows and broader file compatibility.
## [0.6.6] - 2025-05-05
### Added
- 📝 **AI-Enhanced Notes (With Audio Transcription)**: Effortlessly create notes, attach meeting or voice audio, and let the AI instantly enhance, summarize, or refine your notes using audio transcriptions—making your documentation smarter, cleaner, and more insightful with minimal effort.
- 🔊 **Meeting Audio Recording & Import**: Seamlessly record audio from your meetings or capture screen audio and attach it to your notes—making it easier to revisit, annotate, and extract insights from important discussions.
- 📁 **Import Markdown Notes Effortlessly**: Bring your existing knowledge library into Open WebUI by importing your Markdown notes, so you can leverage all advanced note management and AI features right away.
- 👥 **Notes Permissions by User Group**: Fine-tune access and editing rights for notes based on user roles or groups, so you can delegate writing or restrict sensitive information as needed.
- ☁️ **OneDrive & SharePoint Integration**: Keep your content in sync by connecting notes and files directly with OneDrive or SharePoint—unlocking fast enterprise import/export and seamless collaboration with your existing workflows.
- 🗂️ **Paginated User List in Admin Panel**: Effortlessly manage and search through large teams via the new paginated user list—saving time and streamlining user administration in big organizations.
- 🕹️ **Granular Chat Share & Export Permissions**: Enjoy enhanced control over who can share or export chats, enabling tighter governance and privacy in team and enterprise settings.
- 🛑 **User Role Change Confirmation Dialog**: Reduce accidental privilege changes with a required confirmation step before updating user roles—improving security and preventing costly mistakes in team management.
- 🚨 **Audit Log for Failed Login Attempts**: Quickly detect unauthorized access attempts or troubleshoot user login problems with detailed logs of failed authentication right in the audit trail.
- 💡 **Dedicated 'Generate Title' Button for Chats**: Swiftly organize every conversation—tap the new button to let AI create relevant, clear titles for all your chats, saving time and reducing clutter.
- 💬 **Notification Sound Always-On Option**: Take control of your notifications by setting sound alerts to always play—helping you stay on top of important updates in busy environments.
- 🆔 **S3 File Tagging Support**: Uploaded files to S3 now include tags for better organization, searching, and integration with your file management policies.
- 🛡️ **OAuth Blocked Groups Support**: Gain more control over group-based access by explicitly blocking specified OAuth groups—ideal for complex identity or security requirements.
- 🚀 **Optimized Faster Web Search & Multi-Threaded Queries**: Enjoy dramatically faster web search and RAG (retrieval augmented generation) with revamped multi-threaded search—get richer, more accurate results in less time.
- 🔍 **All-Knowledge Parallel Search**: Searches across your entire knowledge base now happen in parallel even in non-hybrid mode, speeding up responses and improving knowledge accuracy for every question.
- 🌐 **New Firecrawl & Yacy Web Search Integrations**: Expand your world of information with two new advanced search engines—Firecrawl for deeper web insight and Yacy for decentralized, privacy-friendly search capabilities.
- 🧠 **Configurable Docling OCR Engine & Language**: Use environment variables to fine-tune Docling OCR engine and supported languages for smarter, more tailored document extraction and RAG workflows.
- 🗝️ **Enhanced Sentence Transformers Configuration**: Added new environment variables for easier set up and advanced customization of Sentence Transformers—ensuring best fit for your embedding needs.
- 🌲 **Pinecone Vector Database Integration**: Index, search, and manage knowledge at enterprise scale with full native support for Pinecone as your vector database—effortlessly handle even the biggest document sets.
- 🔄 **Automatic Requirements Installation for Tools & Functions**: Never worry about lost dependencies on restart—external function and tool requirements are now auto-installed at boot, ensuring tools always “just work.”
- 🔒 **Automatic Sign-Out on Token Expiry**: Security is smarter—users are now automatically logged out if their authentication token expires, protecting sensitive content and ensuring compliance without disruption.
- 🎬 **Automatic YouTube Embed Detection**: Paste YouTube links and see instant in-chat video embeds—no more manual embedding, making knowledge sharing and media consumption even easier for every team.
- 🔄 **Expanded Language & Locale Support**: Translations for Danish, French, Russian, Traditional Chinese, Simplified Chinese, Thai, Catalan, German, and Korean have been upgraded, offering smoother, more natural user experiences across the platform.
### Fixed
- 🔒 **Tighter HTML Token Security**: HTML rendering is now restricted to admin-uploaded tokens only, reducing any risk of XSS and keeping your data safe.
- 🔐 **Refined HTML Security and Token Handling**: Further hardened how HTML tokens and content are handled, guaranteeing even stronger resistance to security vulnerabilities and attacks.
- 🔏 **Correct Model Usage with Ollama Proxy Prefixes**: Enhanced model reference handling so proxied models in Ollama always download and run correctly—even when using custom prefixes.
- 📥 **Video File Upload Handling**: Prevented video files from being misclassified as text, fixing bugs with uploads and ensuring media files work as expected.
- 🔄 **No More Dependent WebSocket Sequential Delays**: Streamlined WebSocket operation to prevent delays and maintain snappy real-time collaboration, especially in multi-user environments.
- 🛠️ **More Robust Action Module Execution**: Multiple actions in a module now trigger as designed, increasing automation and scripting flexibility.
- 📧 **Notification Webhooks**: Ensured that notification webhooks are always sent for user events, even when the user isn’t currently active.
- 🗂️ **Smarter Knowledge Base Reindexing**: Knowledge reindexing continues even when corrupt or missing collections are encountered, keeping your search features running reliably.
- 🏷️ **User Import with Profile Images**: When importing users, their profile images now come along—making onboarding and collaboration visually clearer from day one.
- 💬 **OpenAI o-Series Universal Support**: All OpenAI o-series models are now seamlessly recognized and supported, unlocking more advanced capabilities and model choices for every workflow.
### Changed
- 📜 **Custom License Update & Contributor Agreement**: Open WebUI now operates under a custom license with Contributor License Agreement required by default—see https://docs.openwebui.com/license/ for details, ensuring sustainable open innovation for the community.
- 🔨 **CUDA Docker Images Updated to 12.8**: Upgraded CUDA image support for faster, more compatible model inference and futureproof GPU performance in your AI infrastructure.
- 🧱 **General Backend Refactoring for Reliability**: Continuous stability improvements streamline backend logic, reduce errors, and lay a stronger foundation for the next wave of feature releases—all under the hood for a more dependable WebUI.
## [0.6.5] - 2025-04-14
### Added
- 🛂 **Granular Voice Feature Permissions Per User Group**: Admins can now separately manage access to Speech-to-Text (record voice), Text-to-Speech (read aloud), and Tool Calls for each user group—giving teams tighter control over voice features and enhanced governance across roles.
- 🗣️ **Toggle Voice Activity Detection (VAD) for Whisper STT**: New environment variable lets you enable/disable VAD filtering with built-in Whisper speech-to-text, giving you flexibility to optimize for different audio quality and response accuracy levels.
- 📋 **Copy Formatted Response Mode**: You can now enable “Copy Formatted” in Settings > Interface to copy AI responses exactly as styled (with rich formatting, links, and structure preserved), making it faster and cleaner to paste into documents, emails, or reports.
- ⚙️ **Backend Stability and Performance Enhancements**: General backend refactoring improves system resilience, consistency, and overall reliability—offering smoother performance across workflows whether chatting, generating media, or using external tools.
- 🌎 **Translation Refinements Across Multiple Languages**: Updated translations deliver smoother language localization, clearer labels, and improved international usability throughout the UI—ensuring a better experience for non-English speakers.
### Fixed
- 🛠️ **LDAP Login Reliability Restored**: Resolved a critical issue where some LDAP setups failed due to attribute parsing—ensuring consistent, secure, and seamless user authentication across enterprise deployments.
- 🖼️ **Image Generation in Temporary Chats Now Works Properly**: Fixed a bug where image outputs weren’t generated during temporary chats—visual content can now be used reliably in all chat modes without interruptions.
## [0.6.4] - 2025-04-12
### Fixed
- 🛠️ **RAG_TEMPLATE Display Issue Resolved**: Fixed a formatting problem where the custom RAG_TEMPLATE wasn't correctly rendered in the interface—ensuring that custom retrieval prompts now appear exactly as intended for more reliable prompt engineering.
## [0.6.3] - 2025-04-12
### Added
- 🧪 **Auto-Artifact Detection Toggle**: Automatically detects artifacts in results—but now you can disable this behavior under advanced settings for full control.
- 🖼️ **Widescreen Mode for Shared Chats**: Shared link conversations now support widescreen layouts—perfect for presentations or easier review across wider displays.
- 🔁 **Reindex Knowledge Files on Demand**: Admins can now trigger reindexing of all knowledge files after changing embeddings—ensuring immediate alignment with new models for optimal RAG performance.
- 📄 **OpenAPI YAML Format Support**: External tools can now use YAML-format OpenAPI specs—making integration simpler for developers familiar with YAML-based configurations.
- 💬 **Message Content Copy Behavior**: Copy action now excludes 'details' tags—streamlining clipboard content when sharing or pasting summaries elsewhere.
- 🧭 **Sougou Web Search Integration**: New search engine option added—enhancing global relevance and diversity of search sources for multilingual users.
- 🧰 **Frontend Web Loader Engine Configuration**: Admins can now set preferred web loader engine for RAG workflows directly from the frontend—offering more control across setups.
- 👥 **Multi-Model Chat Permission Control**: Admins can manage access to multi-model chats per user group—allowing tighter governance in team environments.
- 🧱 **Persistent Configuration Can Be Disabled**: New environment variable lets advanced users and hosts turn off persistent configs—ideal for volatile or stateless deployments.
- 🧠 **Elixir Code Highlighting Support**: Elixir syntax is now beautifully rendered in code blocks—perfect for developers using this language in AI or automation projects.
- 🌐 **PWA External Manifest URL Support**: You can now define an external manifest.json—integrate Open WebUI seamlessly in managed or proxy-based PWA environments like Cloudflare Zero Trust.
- 🧪 **Azure AI Speech-to-Text Provider Integration**: Easily transcribe large audio files (up to 200MB) with high accuracy using Microsoft's Azure STT—fully configurable in Audio Settings.
- 🔏 **PKCE (Code Challenge Method) Support for OIDC**: Enhance your OIDC login security with Proof Key for Code Exchange—ideal for zero-trust and native client apps.
- ✨ **General UI/UX Enhancements**: Numerous refinements across layout, styling, and tool interactions—reducing visual noise and improving overall usability across key workflows.
- 🌍 **Translation Updates Across Multiple Languages**: Refined Catalan, Russian, Chinese (Simplified & Traditional), Hungarian, and Spanish translations for clearer navigation and instructions globally.
### Fixed
- 💥 **Chat Completion Error with Missing Models Resolved**: Fixed internal server error when referencing a model that doesn’t exist—ensuring graceful fallback and clear error guidance.
- 🔧 **Correct Knowledge Base Citations Restored**: Citations generated by RAG workflows now show accurate references—ensuring verifiability in outputs from sourced content.
- 🎙️ **Broken OGG/WebM Audio Upload Handling for OpenAI Fixed**: Uploading OGG or WebM files now converts properly to WAV before transcription—restoring accurate AI speech recognition workflows.
- 🔐 **Tool Server 'Session' Authentication Restored**: Previously broken session auth on external tool servers is now fully functional—ensuring secure and seamless access to connected tools.
- 🌐 **Folder-Based Chat Rename Now Updates Correctly**: Renaming chats in folders now reflects instantly everywhere—improving chat organization and clarity.
- 📜 **KaTeX Overflow Displays Fixed**: Math expressions now stay neatly within message bounds—preserving layout consistency even with long formulas.
- 🚫 **Stopping Ongoing Chat Fixed**: You can now return to an active (ongoing) chat and stop generation at any time—ensuring full control over sessions.
- 🔧 **TOOL_SERVERS / TOOL_SERVER_CONNECTIONS Indexing Issue Fixed**: Fixed a mismatch between tool lists and their access paths—restoring full function and preventing confusion in tool management.
- 🔐 **LDAP Login Handles Multiple Emails**: When LDAP returns multiple email attributes, the first valid one is now used—ensuring login success and account consistency.
- 🧩 **Model Visibility Toggle Fix**: Toggling model visibility now works even for untouched models—letting admins smoothly manage user access across base models.
- ⚙️ **Cross-Origin manifest.json Now Loads Properly**: Compatibility issues with Cloudflare Zero Trust (and others) resolved, allowing manifest.json to load behind authenticated proxies.
### Changed
- 🔒 **Default Access Scopes Set to Private for All Resources**: Models, tools, and knowledge are now private by default when created—ensuring better baseline security and visibility controls.
- 🧱 **General Backend Refactoring for Stability**: Numerous invisible improvements enhance backend scalability, security, and maintainability—powering upcoming features with a stronger foundation.
- 🧩 **Stable Dependency Upgrades**: Updated key platform libraries—Chromadb (0.6.3), pgvector (0.4.0), Azure Identity (1.21.0), and Youtube Transcript API (1.0.3)—for improved compatibility, functionality, and security.
## [0.6.2] - 2025-04-06
### Added
- 🌍 **Improved Global Language Support**: Expanded and refined translations across multiple languages to enhance clarity and consistency for international users.
### Fixed
- 🛠️ **Accurate Tool Descriptions from OpenAPI Servers**: External tools now use full endpoint descriptions instead of summaries when generating tool specifications—helping AI models understand tool purpose more precisely and choose the right tool more accurately in tool workflows.
- 🔧 **Precise Web Results Source Attribution**: Fixed a key issue where all web search results showed the same source ID—now each result gets its correct and distinct source, ensuring accurate citations and traceability.
- 🔍 **Clean Web Search Retrieval**: Web search now retains only results from URLs where real content was successfully fetched—improving accuracy and removing empty or broken links from citations.
- 🎵 **Audio File Upload Response Restored**: Resolved an issue where uploading audio files did not return valid responses, restoring smooth file handling for transcription and audio-based workflows.
### Changed
- 🧰 **General Backend Refactoring**: Multiple behind-the-scenes improvements streamline backend performance, reduce complexity, and ensure a more stable, maintainable system overall—making everything smoother without changing your workflow.
## [0.6.1] - 2025-04-05
### Added
- 🛠️ **Global Tool Servers Configuration**: Admins can now centrally configure global external tool servers from Admin Settings > Tools, allowing seamless sharing of tool integrations across all users without manual setup per user.
- 🔐 **Direct Tool Usage Permission for Users**: Introduced a new user-level permission toggle that grants non-admin users access to direct external tools, empowering broader team collaboration while maintaining control.
- 🧠 **Mistral OCR Content Extraction Support**: Added native support for Mistral OCR as a high-accuracy document loader, drastically improving text extraction from scanned documents in RAG workflows.
- 🖼️ **Tools Indicator UI Redesign**: Enhanced message input now smartly displays both built-in and external tools via a unified dropdown, making it simpler and more intuitive to activate tools during conversations.
- 📄 **RAG Prompt Improved and More Coherent**: Default RAG system prompt has been revised to be more clear and citation-focused—admins can leave the template field empty to use this new gold-standard prompt.
- 🧰 **Performance & Developer Improvements**: Major internal restructuring of several tool-related components, simplifying styling and merging external/internal handling logic, resulting in better maintainability and performance.
- 🌍 **Improved Translations**: Updated translations for Tibetan, Polish, Chinese (Simplified & Traditional), Arabic, Russian, Ukrainian, Dutch, Finnish, and French to improve clarity and consistency across the interface.
### Fixed
- 🔑 **External Tool Server API Key Bug Resolved**: Fixed a critical issue where authentication headers were not being sent when calling tools from external OpenAPI tool servers, ensuring full security and smooth tool operations.
- 🚫 **Conditional Export Button Visibility**: UI now gracefully hides export buttons when there's nothing to export in models, prompts, tools, or functions, improving visual clarity and reducing confusion.
- 🧪 **Hybrid Search Failure Recovery**: Resolved edge case in parallel hybrid search where empty or unindexed collections caused backend crashes—these are now cleanly skipped to ensure system stability.
- 📂 **Admin Folder Deletion Fix**: Addressed an issue where folders created in the admin workspace couldn't be deleted, restoring full organizational flexibility for admins.
- 🔐 **Improved Generic Error Feedback on Login**: Authentication errors now show simplified, non-revealing messages for privacy and improved UX, especially with federated logins.
- 📝 **Tool Message with Images Improved**: Enhanced how tool-generated messages with image outputs are shown in chat, making them more readable and consistent with the overall UI design.
- ⚙️ **Auto-Exclusion for Broken RAG Collections**: Auto-skips document collections that fail to fetch data or return "None", preventing silent errors and streamlining retrieval workflows.
- 📝 **Docling Text File Handling Fix**: Fixed file parsing inconsistency that broke docling-based RAG functionality for certain plain text files, ensuring wider file compatibility.
## [0.6.0] - 2025-03-31
### Added
- 🧩 **External Tool Server Support via OpenAPI**: Connect Open WebUI to any OpenAPI-compatible REST server instantly—offering immediate integration with thousands of developer tools, SDKs, and SaaS systems for powerful extensibility. Learn more: https://github.com/open-webui/openapi-servers
- 🛠️ **MCP Server Support via MCPO**: You can now convert and expose your internal MCP tools as interoperable OpenAPI HTTP servers within Open WebUI for seamless, plug-n-play AI toolchain creation. Learn more: https://github.com/open-webui/mcpo
- 📨 **/messages Chat API Endpoint Support**: For power users building external AI systems, new endpoints allow precise control of messages asynchronously—feed long-running external responses into Open WebUI chats without coupling with the frontend.
- 📝 **Client-Side PDF Generation**: PDF exports are now generated fully client-side for drastically improved output quality—perfect for saving conversations or documents.
- 💼 **Enforced Temporary Chats Mode**: Admins can now enforce temporary chat sessions by default to align with stringent data retention and compliance requirements.
- 🌍 **Public Resource Sharing Permission Controls**: Fine-grained user group permissions now allow enabling/disabling public sharing for models, knowledge, prompts, and tools—ideal for privacy, team control, and internal deployments.
- 📦 **Custom pip Options for Tools/Functions**: You can now specify custom pip installation options with "PIP_OPTIONS", "PIP_PACKAGE_INDEX_OPTIONS" environment variables—improving compatibility, support for private indexes, and better control over Python environments.
- 🔢 **Editable Message Counter**: You can now double-click the message count number and jump straight to editing the index—quickly navigate complex chats or regenerate specific messages precisely.
- 🧠 **Embedding Prefix Support Added**: Add custom prefixes to your embeddings for instruct-style tokens, enabling stronger model alignment and more consistent RAG performance.
- 🙈 **Ability to Hide Base Models**: Optionally hide base models from the UI, helping users streamline model visibility and limit access to only usable endpoints..
- 📚 **Docling Content Extraction Support**: Open WebUI now supports Docling as a content extraction engine, enabling smarter and more accurate parsing of complex file formats—ideal for advanced document understanding and Retrieval-Augmented Generation (RAG) workflows.
- 🗃️ **Redis Sentinel Support Added**: Enhance deployment redundancy with support for Redis Sentinel for highly available, failover-safe Redis-based caching or pub/sub.
- 📚 **JSON Schema Format for Ollama**: Added support for defining the format using JSON schema in Ollama-compatible models, improving flexibility and validation of model outputs.
- 🔍 **Chat Sidebar Search "Clear” Button**: Quickly clear search filters in chat sidebar using the new ✖️ button—streamline your chat navigation with one click.
- 🗂️ **Auto-Focus + Enter Submit for Folder Name**: When creating a new folder, the system automatically enters rename mode with name preselected—simplifying your org workflow.
- 🧱 **Markdown Alerts Rendering**: Blockquotes with syntax hinting (e.g. ⚠️, ℹ️, ✅) now render styled Markdown alert banners, making messages and documentation more visually structured.
- 🔁 **Hybrid Search Runs in Parallel Now**: Hybrid (BM25 + embedding) search components now run in parallel—dramatically reducing response times and speeding up document retrieval.
- 📋 **Cleaner UI for Tool Call Display**: Optimized the visual layout of called tools inside chat messages for better clarity and reduced visual clutter.
- 🧪 **Playwright Timeout Now Configurable**: Default timeout for Playwright processes is now shorter and adjustable via environment variables—making web scraping more robust and tunable to environments.
- 📈 **OpenTelemetry Support for Observability**: Open WebUI now integrates with OpenTelemetry, allowing you to connect with tools like Grafana, Jaeger, or Prometheus for detailed performance insights and real-time visibility—entirely opt-in and fully self-hosted. Even if enabled, no data is ever sent to us, ensuring your privacy and ownership over all telemetry data.
- 🛠 **General UI Enhancements & UX Polish**: Numerous refinements across sidebar, code blocks, modal interactions, button alignment, scrollbar visibility, and folder behavior improve overall fluidity and usability of the interface.
- 🧱 **General Backend Refactoring**: Numerous backend components have been refactored to improve stability, maintainability, and performance—ensuring a more consistent and reliable system across all features.
- 🌍 **Internationalization Language Support Updates**: Added Estonian and Galician languages, improved Spanish (fully revised), Traditional Chinese, Simplified Chinese, Turkish, Catalan, Ukrainian, and German for a more localized and inclusive interface.
### Fixed
- 🧑‍💻 **Firefox Input Height Bug**: Text input in Firefox now maintains proper height, ensuring message boxes look consistent and behave predictably.
- 🧾 **Tika Blank Line Bug**: PDFs processed with Apache Tika 3.1.0.0 no longer introduce excessive blank lines—improving RAG output quality and visual cleanliness.
- 🧪 **CSV Loader Encoding Issues**: CSV files with unknown encodings now automatically detect character sets, resolving import errors in non-UTF-8 datasets.
- ✅ **LDAP Auth Config Fix**: Path to certificate file is now optional for LDAP setups, fixing authentication trouble for users without preconfigured cert paths.
- 📥 **File Deletion in Bypass Mode**: Resolved issue where files couldn’t be deleted from knowledge when “bypass embedding” mode was enabled.
- 🧩 **Hybrid Search Result Sorting & Deduplication Fixed**: Fixed citation and sorting issues in RAG hybrid and reranker modes, ensuring retrieved documents are shown in correct order per score.
- 🧷 **Model Export/Import Broken for a Single Model**: Fixed bug where individual models couldn’t be exported or re-imported, restoring full portability.
- 📫 **Auth Redirect Fix**: Logged-in users are now routed properly without unnecessary login prompts when already authenticated.
### Changed
- 🧠 **Prompt Autocompletion Disabled By Default**: Autocomplete suggestions while typing are now disabled unless explicitly re-enabled in user preferences—reduces distractions while composing prompts for advanced users.
- 🧾 **Normalize Citation Numbering**: Source citations now properly begin from "1" instead of "0"—improving consistency and professional presentation in AI outputs.
- 📚 **Improved Error Handling from Pipelines**: Pipelines now show the actual returned error message from failed tasks rather than generic "Connection closed"—making debugging far more user-friendly.
### Removed
- 🧾 **ENABLE_AUDIT_LOGS Setting Removed**: Deprecated setting “ENABLE_AUDIT_LOGS” has been fully removed—now controlled via “AUDIT_LOG_LEVEL” instead.
## [0.5.20] - 2025-03-05
### Added
- **⚡ Toggle Code Execution On/Off**: You can now enable or disable code execution, providing more control over security, ensuring a safer and more customizable experience.
### Fixed
- **📜 Pinyin Keyboard Enter Key Now Works Properly**: Resolved an issue where the Enter key for Pinyin keyboards was not functioning as expected, ensuring seamless input for Chinese users.
- **🖼️ Web Manifest Loading Issue Fixed**: Addressed inconsistencies with 'site.webmanifest', guaranteeing proper loading and representation of the app across different browsers and devices.
- **📦 Non-Root Container Issue Resolved**: Fixed a critical issue where the UI failed to load correctly in non-root containers, ensuring reliable deployment in various environments.
## [0.5.19] - 2025-03-04
### Added
- **📊 Logit Bias Parameter Support**: Fine-tune conversation dynamics by adjusting the Logit Bias parameter directly in chat settings, giving you more control over model responses.
- **⌨️ Customizable Enter Behavior**: You can now configure Enter to send messages only when combined with Ctrl (Ctrl+Enter) via Settings > Interface, preventing accidental message sends.
- **📝 Collapsible Code Blocks**: Easily collapse long code blocks to declutter your chat, making it easier to focus on important details.
- **🏷️ Tag Selector in Model Selector**: Quickly find and categorize models with the new tag filtering system in the Model Selector, streamlining model discovery.
- **📈 Experimental Elasticsearch Vector DB Support**: Now supports Elasticsearch as a vector database, offering more flexibility for data retrieval in Retrieval-Augmented Generation (RAG) workflows.
- **⚙️ General Reliability Enhancements**: Various stability improvements across the WebUI, ensuring a smoother, more consistent experience.
- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages.
### Fixed
- **🔄 "Stream" Hook Activation**: Fixed an issue where the "Stream" hook only worked when globally enabled, ensuring reliable real-time filtering.
- **📧 LDAP Email Case Sensitivity**: Resolved an issue where LDAP login failed due to email case sensitivity mismatches, improving authentication reliability.
- **💬 WebSocket Chat Event Registration**: Fixed a bug preventing chat event listeners from being registered upon sign-in, ensuring real-time updates work properly.
## [0.5.18] - 2025-02-27
### Fixed
+3 -3
View File
@@ -2,13 +2,13 @@
## Our Pledge
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
As members, contributors, and leaders of this community, we pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
## Why These Standards Are Important
Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
Projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.**
@@ -79,7 +79,7 @@ This approach ensures that disruptive behaviors are addressed swiftly and decisi
## Why Zero Tolerance Is Necessary
Open-source projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
Projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive.
+7
View File
@@ -0,0 +1,7 @@
# Open WebUI Contributor License Agreement
By submitting my contributions to Open WebUI, I grant Open WebUI full freedom to use my work in any way they choose, under any terms they like, both now and in the future. This approach helps ensure the project remains unified, flexible, and easy to maintain, while empowering Open WebUI to respond quickly to the needs of its users and the wider community.
Taking part in this process means my work can be seamlessly integrated and combined with others, ensuring longevity and adaptability for everyone who benefits from the Open WebUI project. This collaborative approach strengthens the project’s future and helps guarantee that improvements can always be shared and distributed in the most effective way possible.
**_To the fullest extent permitted by law, my contributions are provided on an “as is” basis, with no warranties or guarantees of any kind, and I disclaim any liability for any issues or damages arising from their use or incorporation into the project, regardless of the type of legal claim._**
-64
View File
@@ -1,64 +0,0 @@
# Run with
# caddy run --envfile ./example.env --config ./Caddyfile.localhost
#
# This is configured for
# - Automatic HTTPS (even for localhost)
# - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api)
# - CORS
# - HTTP Basic Auth API Tokens (uncomment basicauth section)
# CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
(cors-api) {
@match-cors-api-preflight method OPTIONS
handle @match-cors-api-preflight {
header {
Access-Control-Allow-Origin "{http.request.header.origin}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
Access-Control-Allow-Credentials "true"
Access-Control-Max-Age "3600"
defer
}
respond "" 204
}
@match-cors-api-request {
not {
header Origin "{http.request.scheme}://{http.request.host}"
}
header Origin "{http.request.header.origin}"
}
handle @match-cors-api-request {
header {
Access-Control-Allow-Origin "{http.request.header.origin}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
Access-Control-Allow-Credentials "true"
Access-Control-Max-Age "3600"
defer
}
}
}
# replace localhost with example.com or whatever
localhost {
## HTTP Basic Auth
## (uncomment to enable)
# basicauth {
# # see .example.env for how to generate tokens
# {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST}
# }
handle /api/* {
# Comment to disable CORS
import cors-api
reverse_proxy localhost:11434
}
# Same-Origin Static Web Server
file_server {
root ./build/
}
}
+2 -2
View File
@@ -4,7 +4,7 @@
ARG USE_CUDA=false
ARG USE_OLLAMA=false
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
ARG USE_CUDA_VER=cu121
ARG USE_CUDA_VER=cu128
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
@@ -132,7 +132,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
# install python dependencies
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
RUN pip3 install uv && \
RUN pip3 install --no-cache-dir uv && \
if [ "$USE_CUDA" = "true" ]; then \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
+7 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2023-2025 Timothy Jaeryang Baek
Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI)
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -15,6 +15,12 @@ modification, are permitted provided that the following conditions are met:
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license.
6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+20 -4
View File
@@ -7,7 +7,6 @@
![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
@@ -62,9 +61,26 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
## 🔗 Also Check Out Open WebUI Community!
## Sponsors 🙌
Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Open WebUI! 🚀
#### Emerald
<table>
<tr>
<td>
<a href="https://n8n.io/" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/n8n.png" alt="n8n" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
Does your interface have a backend yet?<br>Try <a href="https://n8n.io/">n8n</a>
</td>
</tr>
</table>
---
We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
## How to Install 🚀
@@ -206,7 +222,7 @@ Discover upcoming features on our roadmap in the [Open WebUI Documentation](http
## License 📜
This project is licensed under the [BSD-3-Clause License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
This project is licensed under the [Open WebUI License](LICENSE), a revised BSD-3-Clause license. You receive all the same rights as the classic BSD-3 license: you can use, modify, and distribute the software, including in proprietary and commercial products, with minimal restrictions. The only additional requirement is to preserve the "Open WebUI" branding, as detailed in the LICENSE file. For full terms, see the [LICENSE](LICENSE) document. 📄
## Support 💬
+8 -1
View File
@@ -73,8 +73,15 @@ def serve(
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
import open_webui.main # we need set environment variables before importing main
from open_webui.env import UVICORN_WORKERS # Import the workers setting
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
uvicorn.run(
"open_webui.main:app",
host=host,
port=port,
forwarded_allow_ips="*",
workers=UVICORN_WORKERS,
)
@app.command()
File diff suppressed because it is too large Load Diff
+1
View File
@@ -31,6 +31,7 @@ class ERROR_MESSAGES(str, Enum):
USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username."
)
PASSWORD_TOO_LONG = "Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long."
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
+138 -22
View File
@@ -65,10 +65,8 @@ except Exception:
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
@@ -78,6 +76,7 @@ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
if "cuda_error" in locals():
log.exception(cuda_error)
del cuda_error
log_sources = [
"AUDIO",
@@ -100,13 +99,12 @@ SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
if SRC_LOG_LEVELS[source] not in logging.getLevelNamesMapping():
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
@@ -131,7 +129,6 @@ else:
except Exception:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
@@ -162,7 +159,6 @@ try:
except Exception:
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
# Convert markdown content to HTML
html_content = markdown.markdown(changelog_content)
@@ -193,7 +189,6 @@ for version in soup.find_all("h2"):
changelog_json[version_number] = version_data
CHANGELOG = changelog_json
####################################
@@ -210,7 +205,6 @@ ENABLE_FORWARD_USER_INFO_HEADERS = (
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
)
####################################
# WEBUI_BUILD_HASH
####################################
@@ -245,7 +239,6 @@ if FROM_INIT_PY:
DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static"))
FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
@@ -257,7 +250,6 @@ if FROM_INIT_PY:
os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
).resolve()
####################################
# Database
####################################
@@ -322,7 +314,6 @@ RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
ENABLE_REALTIME_CHAT_SAVE = (
os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
)
@@ -331,7 +322,23 @@ ENABLE_REALTIME_CHAT_SAVE = (
# REDIS
####################################
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
REDIS_URL = os.environ.get("REDIS_URL", "")
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
####################################
# UVICORN WORKERS
####################################
# Number of uvicorn worker processes for handling requests
UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1")
try:
UVICORN_WORKERS = int(UVICORN_WORKERS)
if UVICORN_WORKERS < 1:
UVICORN_WORKERS = 1
except ValueError:
UVICORN_WORKERS = 1
log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}")
####################################
# WEBUI_AUTH (Required for security)
@@ -347,6 +354,10 @@ BYPASS_MODEL_ACCESS_CONTROL = (
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
)
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get(
"WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None
)
####################################
# WEBUI_SECRET_KEY
####################################
@@ -386,6 +397,11 @@ ENABLE_WEBSOCKET_SUPPORT = (
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
WEBSOCKET_REDIS_LOCK_TIMEOUT = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", 60)
WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "")
WEBSOCKET_SENTINEL_PORT = os.environ.get("WEBSOCKET_SENTINEL_PORT", "26379")
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
@@ -397,19 +413,88 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
AIOHTTP_CLIENT_SESSION_SSL = (
os.environ.get("AIOHTTP_CLIENT_SESSION_SSL", "True").lower() == "true"
)
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "10"),
)
if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "":
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
except Exception:
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10"
)
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "":
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
)
except Exception:
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
os.environ.get("AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL", "True").lower() == "true"
)
####################################
# SENTENCE TRANSFORMERS
####################################
SENTENCE_TRANSFORMERS_BACKEND = os.environ.get("SENTENCE_TRANSFORMERS_BACKEND", "")
if SENTENCE_TRANSFORMERS_BACKEND == "":
SENTENCE_TRANSFORMERS_BACKEND = "torch"
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get(
"SENTENCE_TRANSFORMERS_MODEL_KWARGS", ""
)
if SENTENCE_TRANSFORMERS_MODEL_KWARGS == "":
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
else:
try:
SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(
SENTENCE_TRANSFORMERS_MODEL_KWARGS
)
except Exception:
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND", ""
)
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == "":
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = "torch"
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS", ""
)
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == "":
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
else:
try:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS
)
except Exception:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
####################################
# OFFLINE_MODE
@@ -420,16 +505,16 @@ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
if OFFLINE_MODE:
os.environ["HF_HUB_OFFLINE"] = "1"
####################################
# AUDIT LOGGING
####################################
ENABLE_AUDIT_LOGS = os.getenv("ENABLE_AUDIT_LOGS", "false").lower() == "true"
# Where to store log file
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
# Maximum size of a file before rotating into a new log file
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
# METADATA | REQUEST | REQUEST_RESPONSE
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "REQUEST_RESPONSE").upper()
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper()
try:
MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048)
except ValueError:
@@ -441,3 +526,34 @@ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders"
)
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
####################################
# OPENTELEMETRY
####################################
ENABLE_OTEL = os.environ.get("ENABLE_OTEL", "False").lower() == "true"
OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get(
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
)
OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui")
OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
"OTEL_RESOURCE_ATTRIBUTES", ""
) # e.g. key1=val1,key2=val2
OTEL_TRACES_SAMPLER = os.environ.get(
"OTEL_TRACES_SAMPLER", "parentbased_always_on"
).lower()
####################################
# TOOLS/FUNCTIONS PIP OPTIONS
####################################
PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
####################################
# PROGRESSIVE WEB APP OPTIONS
####################################
EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
+3
View File
@@ -223,6 +223,9 @@ async def generate_function_chat_completion(
extra_params = {
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
"__chat_id__": metadata.get("chat_id", None),
"__session_id__": metadata.get("session_id", None),
"__message_id__": metadata.get("message_id", None),
"__task__": __task__,
"__task_body__": __task_body__,
"__files__": files,
+261 -66
View File
@@ -17,6 +17,7 @@ from sqlalchemy import text
from typing import Optional
from aiocache import cached
import aiohttp
import anyio.to_thread
import requests
@@ -63,6 +64,7 @@ from open_webui.routers import (
auths,
channels,
chats,
notes,
folders,
configs,
groups,
@@ -84,11 +86,12 @@ from open_webui.routers.retrieval import (
get_rf,
)
from open_webui.internal.db import Session
from open_webui.internal.db import Session, engine
from open_webui.models.functions import Functions
from open_webui.models.models import Models
from open_webui.models.users import UserModel, Users
from open_webui.models.chats import Chats
from open_webui.config import (
LICENSE_KEY,
@@ -99,12 +102,19 @@ from open_webui.config import (
# OpenAI
ENABLE_OPENAI_API,
ONEDRIVE_CLIENT_ID,
ONEDRIVE_SHAREPOINT_URL,
ONEDRIVE_SHAREPOINT_TENANT_ID,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
OPENAI_API_CONFIGS,
# Direct Connections
ENABLE_DIRECT_CONNECTIONS,
# Thread pool size for FastAPI/AnyIO
THREAD_POOL_SIZE,
# Tool Server Configs
TOOL_SERVER_CONNECTIONS,
# Code Execution
ENABLE_CODE_EXECUTION,
CODE_EXECUTION_ENGINE,
CODE_EXECUTION_JUPYTER_URL,
CODE_EXECUTION_JUPYTER_AUTH,
@@ -144,6 +154,11 @@ from open_webui.config import (
AUDIO_STT_MODEL,
AUDIO_STT_OPENAI_API_BASE_URL,
AUDIO_STT_OPENAI_API_KEY,
AUDIO_STT_AZURE_API_KEY,
AUDIO_STT_AZURE_REGION,
AUDIO_STT_AZURE_LOCALES,
AUDIO_STT_AZURE_BASE_URL,
AUDIO_STT_AZURE_MAX_SPEAKERS,
AUDIO_TTS_API_KEY,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
@@ -152,12 +167,16 @@ from open_webui.config import (
AUDIO_TTS_SPLIT_ON,
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_BASE_URL,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
PLAYWRIGHT_WS_URI,
PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
RAG_WEB_LOADER_ENGINE,
WEB_LOADER_ENGINE,
WHISPER_MODEL,
WHISPER_VAD_FILTER,
WHISPER_LANGUAGE,
DEEPGRAM_API_KEY,
WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR,
@@ -169,7 +188,10 @@ from open_webui.config import (
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
RAG_RERANKING_ENGINE,
RAG_RERANKING_MODEL,
RAG_EXTERNAL_RERANKER_URL,
RAG_EXTERNAL_RERANKER_API_KEY,
RAG_RERANKING_MODEL_AUTO_UPDATE,
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
RAG_EMBEDDING_ENGINE,
@@ -185,36 +207,49 @@ from open_webui.config import (
CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL,
DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_KEY,
RAG_TOP_K,
RAG_TOP_K_RERANKER,
RAG_TEXT_SPLITTER,
TIKTOKEN_ENCODING_NAME,
PDF_EXTRACT_IMAGES,
YOUTUBE_LOADER_LANGUAGE,
YOUTUBE_LOADER_PROXY_URL,
# Retrieval (Web Search)
RAG_WEB_SEARCH_ENGINE,
ENABLE_WEB_SEARCH,
WEB_SEARCH_ENGINE,
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_WEB_SEARCH_TRUST_ENV,
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
WEB_SEARCH_RESULT_COUNT,
WEB_SEARCH_CONCURRENT_REQUESTS,
WEB_SEARCH_TRUST_ENV,
WEB_SEARCH_DOMAIN_FILTER_LIST,
JINA_API_KEY,
SEARCHAPI_API_KEY,
SEARCHAPI_ENGINE,
SERPAPI_API_KEY,
SERPAPI_ENGINE,
SEARXNG_QUERY_URL,
YACY_QUERY_URL,
YACY_USERNAME,
YACY_PASSWORD,
SERPER_API_KEY,
SERPLY_API_KEY,
SERPSTACK_API_KEY,
SERPSTACK_HTTPS,
TAVILY_API_KEY,
TAVILY_EXTRACT_DEPTH,
BING_SEARCH_V7_ENDPOINT,
BING_SEARCH_V7_SUBSCRIPTION_KEY,
BRAVE_SEARCH_API_KEY,
EXA_API_KEY,
PERPLEXITY_API_KEY,
SOUGOU_API_SID,
SOUGOU_API_SK,
KAGI_SEARCH_API_KEY,
MOJEEK_SEARCH_API_KEY,
BOCHA_SEARCH_API_KEY,
@@ -223,13 +258,18 @@ from open_webui.config import (
GOOGLE_DRIVE_CLIENT_ID,
GOOGLE_DRIVE_API_KEY,
ONEDRIVE_CLIENT_ID,
ONEDRIVE_SHAREPOINT_URL,
ONEDRIVE_SHAREPOINT_TENANT_ID,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
ENABLE_RAG_WEB_SEARCH,
ENABLE_WEB_LOADER_SSL_VERIFICATION,
ENABLE_GOOGLE_DRIVE_INTEGRATION,
ENABLE_ONEDRIVE_INTEGRATION,
UPLOAD_DIR,
EXTERNAL_WEB_SEARCH_URL,
EXTERNAL_WEB_SEARCH_API_KEY,
EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY,
# WebUI
WEBUI_AUTH,
WEBUI_NAME,
@@ -244,8 +284,10 @@ from open_webui.config import (
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
API_KEY_ALLOWED_ENDPOINTS,
ENABLE_CHANNELS,
ENABLE_NOTES,
ENABLE_COMMUNITY_SHARING,
ENABLE_MESSAGE_RATING,
ENABLE_USER_WEBHOOKS,
ENABLE_EVALUATION_ARENA_MODELS,
USER_PERMISSIONS,
DEFAULT_USER_ROLE,
@@ -310,6 +352,9 @@ from open_webui.env import (
AUDIT_EXCLUDED_PATHS,
AUDIT_LOG_LEVEL,
CHANGELOG,
REDIS_URL,
REDIS_SENTINEL_HOSTS,
REDIS_SENTINEL_PORT,
GLOBAL_LOG_LEVEL,
MAX_BODY_LOG_SIZE,
SAFE_MODE,
@@ -321,10 +366,13 @@ from open_webui.env import (
WEBUI_SESSION_COOKIE_SECURE,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
ENABLE_WEBSOCKET_SUPPORT,
BYPASS_MODEL_ACCESS_CONTROL,
RESET_CONFIG_ON_START,
OFFLINE_MODE,
ENABLE_OTEL,
EXTERNAL_PWA_MANIFEST_URL,
)
@@ -343,14 +391,22 @@ from open_webui.utils.access_control import has_access
from open_webui.utils.auth import (
get_license_data,
get_http_authorization_cred,
decode_token,
get_admin_user,
get_verified_user,
)
from open_webui.utils.plugin import install_tool_and_function_dependencies
from open_webui.utils.oauth import OAuthManager
from open_webui.utils.security_headers import SecurityHeadersMiddleware
from open_webui.tasks import stop_task, list_tasks # Import from tasks.py
from open_webui.tasks import (
list_task_ids_by_chat_id,
stop_task,
list_tasks,
) # Import from tasks.py
from open_webui.utils.redis import get_sentinels_from_env
if SAFE_MODE:
@@ -387,7 +443,7 @@ print(
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝
v{VERSION} - building the best open-source AI user interface.
v{VERSION} - building the best AI user interface.
{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
https://github.com/open-webui/open-webui
"""
@@ -400,14 +456,25 @@ async def lifespan(app: FastAPI):
if RESET_CONFIG_ON_START:
reset_config()
if app.state.config.LICENSE_KEY:
get_license_data(app, app.state.config.LICENSE_KEY)
if LICENSE_KEY:
get_license_data(app, LICENSE_KEY)
# This should be blocking (sync) so functions are not deactivated on first /get_models calls
# when the first user lands on the / route.
log.info("Installing external dependencies of functions and tools...")
install_tool_and_function_dependencies()
if THREAD_POOL_SIZE and THREAD_POOL_SIZE > 0:
limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = THREAD_POOL_SIZE
asyncio.create_task(periodic_usage_pool_cleanup())
yield
app = FastAPI(
title="Open WebUI",
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
@@ -416,10 +483,26 @@ app = FastAPI(
oauth_manager = OAuthManager(app)
app.state.config = AppConfig()
app.state.config = AppConfig(
redis_url=REDIS_URL,
redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
)
app.state.WEBUI_NAME = WEBUI_NAME
app.state.config.LICENSE_KEY = LICENSE_KEY
app.state.LICENSE_METADATA = None
########################################
#
# OPENTELEMETRY
#
########################################
if ENABLE_OTEL:
from open_webui.utils.telemetry.setup import setup as setup_opentelemetry
setup_opentelemetry(app=app, db_engine=engine)
########################################
#
@@ -447,6 +530,15 @@ app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
app.state.OPENAI_MODELS = {}
########################################
#
# TOOL SERVERS
#
########################################
app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
app.state.TOOL_SERVERS = []
########################################
#
# DIRECT CONNECTIONS
@@ -488,8 +580,10 @@ app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
app.state.config.ENABLE_NOTES = ENABLE_NOTES
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
@@ -520,6 +614,8 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL
app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
app.state.USER_COUNT = None
app.state.TOOLS = {}
@@ -533,6 +629,7 @@ app.state.FUNCTIONS = {}
app.state.config.TOP_K = RAG_TOP_K
app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
@@ -541,14 +638,16 @@ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
@@ -559,7 +658,12 @@ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL
app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
@@ -574,16 +678,23 @@ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH
app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE
app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT
app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS
app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE
app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV
app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.YACY_QUERY_URL = YACY_QUERY_URL
app.state.config.YACY_USERNAME = YACY_USERNAME
app.state.config.YACY_PASSWORD = YACY_PASSWORD
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
@@ -603,14 +714,20 @@ app.state.config.JINA_API_KEY = JINA_API_KEY
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
app.state.config.EXA_API_KEY = EXA_API_KEY
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
app.state.config.SOUGOU_API_SID = SOUGOU_API_SID
app.state.config.SOUGOU_API_SK = SOUGOU_API_SK
app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL
app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY
app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL
app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
app.state.config.RAG_WEB_LOADER_ENGINE = RAG_WEB_LOADER_ENGINE
app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV
app.state.config.PLAYWRIGHT_WS_URI = PLAYWRIGHT_WS_URI
app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT
app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
app.state.config.TAVILY_EXTRACT_DEPTH = TAVILY_EXTRACT_DEPTH
app.state.EMBEDDING_FUNCTION = None
app.state.ef = None
@@ -627,7 +744,10 @@ try:
)
app.state.rf = get_rf(
app.state.config.RAG_RERANKING_ENGINE,
app.state.config.RAG_RERANKING_MODEL,
app.state.config.RAG_EXTERNAL_RERANKER_URL,
app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
RAG_RERANKING_MODEL_AUTO_UPDATE,
)
except Exception as e:
@@ -658,6 +778,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
#
########################################
app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
@@ -725,8 +846,15 @@ 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.WHISPER_VAD_FILTER = WHISPER_VAD_FILTER
app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION
app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
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
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
@@ -737,6 +865,7 @@ app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
app.state.config.TTS_AZURE_SPEECH_BASE_URL = AUDIO_TTS_AZURE_SPEECH_BASE_URL
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
@@ -799,7 +928,8 @@ class RedirectMiddleware(BaseHTTPMiddleware):
# Check for the specific watch path and the presence of 'v' parameter
if path.endswith("/watch") and "v" in query_params:
video_id = query_params["v"][0] # Extract the first 'v' parameter
# Extract the first 'v' parameter
video_id = query_params["v"][0]
encoded_video_id = urlencode({"youtube": video_id})
redirect_url = f"/?{encoded_video_id}"
return RedirectResponse(url=redirect_url)
@@ -825,6 +955,10 @@ async def commit_session_after_request(request: Request, call_next):
@app.middleware("http")
async def check_url(request: Request, call_next):
start_time = int(time.time())
request.state.token = get_http_authorization_cred(
request.headers.get("Authorization")
)
request.state.enable_api_key = app.state.config.ENABLE_API_KEY
response = await call_next(request)
process_time = int(time.time()) - start_time
@@ -881,6 +1015,8 @@ app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"])
app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"])
app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"])
app.include_router(models.router, prefix="/api/v1/models", tags=["models"])
app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"])
@@ -943,14 +1079,29 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
return filtered_models
models = await get_all_models(request, user=user)
all_models = await get_all_models(request, user=user)
# Filter out filter pipelines
models = [
model
for model in models
if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
]
models = []
for model in all_models:
# Filter out filter pipelines
if "pipeline" in model and model["pipeline"].get("type", None) == "filter":
continue
try:
model_tags = [
tag.get("name")
for tag in model.get("info", {}).get("meta", {}).get("tags", [])
]
tags = [tag.get("name") for tag in model.get("tags", [])]
tags = list(set(model_tags + tags))
model["tags"] = [{"name": tag} for tag in tags]
except Exception as e:
log.debug(f"Error processing model tags: {e}")
model["tags"] = []
pass
models.append(model)
model_order_list = request.app.state.config.MODEL_ORDER_LIST
if model_order_list:
@@ -988,6 +1139,7 @@ async def chat_completion(
model_item = form_data.pop("model_item", {})
tasks = form_data.pop("background_tasks", None)
metadata = {}
try:
if not model_item.get("direct", False):
model_id = form_data.get("model", None)
@@ -1016,10 +1168,11 @@ async def chat_completion(
"message_id": form_data.pop("id", None),
"session_id": form_data.pop("session_id", None),
"tool_ids": form_data.get("tool_ids", None),
"tool_servers": form_data.pop("tool_servers", None),
"files": form_data.get("files", None),
"features": form_data.get("features", None),
"variables": form_data.get("variables", None),
"model": model_info.model_dump() if model_info else model,
"model": model,
"direct": model_item.get("direct", False),
**(
{"function_calling": "native"}
@@ -1037,11 +1190,21 @@ async def chat_completion(
form_data["metadata"] = metadata
form_data, metadata, events = await process_chat_payload(
request, form_data, metadata, user, model
request, form_data, user, metadata, model
)
except Exception as e:
log.debug(f"Error processing chat payload: {e}")
if metadata.get("chat_id") and metadata.get("message_id"):
# Update the chat message with the error
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"error": {"content": str(e)},
},
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
@@ -1051,7 +1214,7 @@ async def chat_completion(
response = await chat_completion_handler(request, form_data, user)
return await process_chat_response(
request, response, form_data, user, events, metadata, tasks
request, response, form_data, user, metadata, model, events, tasks
)
except Exception as e:
raise HTTPException(
@@ -1106,7 +1269,7 @@ async def chat_action(
@app.post("/api/tasks/stop/{task_id}")
async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
try:
result = await stop_task(task_id) # Use the function from tasks.py
result = await stop_task(task_id)
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
@@ -1114,7 +1277,19 @@ async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
@app.get("/api/tasks")
async def list_tasks_endpoint(user=Depends(get_verified_user)):
return {"tasks": list_tasks()} # Use the function from tasks.py
return {"tasks": list_tasks()}
@app.get("/api/tasks/chat/{chat_id}")
async def list_tasks_by_chat_id_endpoint(chat_id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id(chat_id)
if chat is None or chat.user_id != user.id:
return {"task_ids": []}
task_ids = list_task_ids_by_chat_id(chat_id)
print(f"Task IDs for chat {chat_id}: {task_ids}")
return {"task_ids": task_ids}
##################################
@@ -1140,9 +1315,10 @@ async def get_app_config(request: Request):
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
user_count = Users.get_num_users()
onboarding = False
if user is None:
user_count = Users.get_num_users()
onboarding = user_count == 0
return {
@@ -1169,12 +1345,15 @@ async def get_app_config(request: Request):
{
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
"enable_channels": app.state.config.ENABLE_CHANNELS,
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
"enable_notes": app.state.config.ENABLE_NOTES,
"enable_web_search": app.state.config.ENABLE_WEB_SEARCH,
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
"enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
"enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
"enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
"enable_user_webhooks": app.state.config.ENABLE_USER_WEBHOOKS,
"enable_admin_export": ENABLE_ADMIN_EXPORT,
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
@@ -1188,6 +1367,7 @@ async def get_app_config(request: Request):
{
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"user_count": user_count,
"code": {
"engine": app.state.config.CODE_EXECUTION_ENGINE,
},
@@ -1210,7 +1390,19 @@ async def get_app_config(request: Request):
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
"api_key": GOOGLE_DRIVE_API_KEY.value,
},
"onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value},
"onedrive": {
"client_id": ONEDRIVE_CLIENT_ID.value,
"sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value,
"sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value,
},
"license_metadata": app.state.LICENSE_METADATA,
**(
{
"active_entries": app.state.USER_COUNT,
}
if user.role == "admin"
else {}
),
}
if user is not None
else {}
@@ -1304,29 +1496,32 @@ async def oauth_callback(provider: str, request: Request, response: Response):
@app.get("/manifest.json")
async def get_manifest_json():
return {
"name": app.state.WEBUI_NAME,
"short_name": app.state.WEBUI_NAME,
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"orientation": "natural",
"icons": [
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "any",
},
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "maskable",
},
],
}
if app.state.EXTERNAL_PWA_MANIFEST_URL:
return requests.get(app.state.EXTERNAL_PWA_MANIFEST_URL).json()
else:
return {
"name": app.state.WEBUI_NAME,
"short_name": app.state.WEBUI_NAME,
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"orientation": "any",
"icons": [
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "any",
},
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "maskable",
},
],
}
@app.get("/opensearch.xml")
@@ -0,0 +1,33 @@
"""Add note table
Revision ID: 9f0c9cd09105
Revises: 3781e22d8b01
Create Date: 2025-05-03 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "9f0c9cd09105"
down_revision = "3781e22d8b01"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"note",
sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column("user_id", sa.Text(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("access_control", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
)
def downgrade():
op.drop_table("note")
+1 -1
View File
@@ -436,7 +436,7 @@ class ChatTable:
all_chats = query.all()
# result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
# result has to be destructured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
return [
ChatTitleIdResponse.model_validate(
{
+13 -6
View File
@@ -9,6 +9,8 @@ from open_webui.models.chats import Chats
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
from open_webui.utils.access_control import get_permissions
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -234,15 +236,18 @@ class FolderTable:
log.error(f"update_folder: {e}")
return
def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> bool:
def delete_folder_by_id_and_user_id(
self, id: str, user_id: str, delete_chats=True
) -> bool:
try:
with get_db() as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return False
# Delete all chats in the folder
Chats.delete_chats_by_user_id_and_folder_id(user_id, folder.id)
if delete_chats:
# Delete all chats in the folder
Chats.delete_chats_by_user_id_and_folder_id(user_id, folder.id)
# Delete all children folders
def delete_children(folder):
@@ -250,9 +255,11 @@ class FolderTable:
folder.id, user_id
)
for folder_child in folder_children:
Chats.delete_chats_by_user_id_and_folder_id(
user_id, folder_child.id
)
if delete_chats:
Chats.delete_chats_by_user_id_and_folder_id(
user_id, folder_child.id
)
delete_children(folder_child)
folder = db.query(Folder).filter_by(id=folder_child.id).first()
+3 -2
View File
@@ -63,14 +63,15 @@ class MemoriesTable:
else:
return None
def update_memory_by_id(
def update_memory_by_id_and_user_id(
self,
id: str,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
try:
db.query(Memory).filter_by(id=id).update(
db.query(Memory).filter_by(id=id, user_id=user_id).update(
{"content": content, "updated_at": int(time.time())}
)
db.commit()
+135
View File
@@ -0,0 +1,135 @@
import json
import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.utils.access_control import has_access
from open_webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from sqlalchemy import or_, func, select, and_, text
from sqlalchemy.sql import exists
####################
# Note DB Schema
####################
class Note(Base):
__tablename__ = "note"
id = Column(Text, primary_key=True)
user_id = Column(Text)
title = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class NoteModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
title: str
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
####################
# Forms
####################
class NoteForm(BaseModel):
title: str
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
class NoteUserResponse(NoteModel):
user: Optional[UserResponse] = None
class NoteTable:
def insert_new_note(
self,
form_data: NoteForm,
user_id: str,
) -> Optional[NoteModel]:
with get_db() as db:
note = NoteModel(
**{
"id": str(uuid.uuid4()),
"user_id": user_id,
**form_data.model_dump(),
"created_at": int(time.time_ns()),
"updated_at": int(time.time_ns()),
}
)
new_note = Note(**note.model_dump())
db.add(new_note)
db.commit()
return note
def get_notes(self) -> list[NoteModel]:
with get_db() as db:
notes = db.query(Note).order_by(Note.updated_at.desc()).all()
return [NoteModel.model_validate(note) for note in notes]
def get_notes_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[NoteModel]:
notes = self.get_notes()
return [
note
for note in notes
if note.user_id == user_id
or has_access(user_id, permission, note.access_control)
]
def get_note_by_id(self, id: str) -> Optional[NoteModel]:
with get_db() as db:
note = db.query(Note).filter(Note.id == id).first()
return NoteModel.model_validate(note) if note else None
def update_note_by_id(self, id: str, form_data: NoteForm) -> Optional[NoteModel]:
with get_db() as db:
note = db.query(Note).filter(Note.id == id).first()
if not note:
return None
note.title = form_data.title
note.data = form_data.data
note.meta = form_data.meta
note.access_control = form_data.access_control
note.updated_at = int(time.time_ns())
db.commit()
return NoteModel.model_validate(note) if note else None
def delete_note_by_id(self, id: str):
with get_db() as db:
db.query(Note).filter(Note.id == id).delete()
db.commit()
return True
Notes = NoteTable()
+74 -5
View File
@@ -10,6 +10,8 @@ from open_webui.models.groups import Groups
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import or_
####################
# User DB Schema
@@ -67,6 +69,11 @@ class UserModel(BaseModel):
####################
class UserListResponse(BaseModel):
users: list[UserModel]
total: int
class UserResponse(BaseModel):
id: str
name: str
@@ -160,11 +167,63 @@ class UsersTable:
return None
def get_users(
self, skip: Optional[int] = None, limit: Optional[int] = None
) -> list[UserModel]:
self,
filter: Optional[dict] = None,
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> UserListResponse:
with get_db() as db:
query = db.query(User)
query = db.query(User).order_by(User.created_at.desc())
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
)
)
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "name":
if direction == "asc":
query = query.order_by(User.name.asc())
else:
query = query.order_by(User.name.desc())
elif order_by == "email":
if direction == "asc":
query = query.order_by(User.email.asc())
else:
query = query.order_by(User.email.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(User.created_at.asc())
else:
query = query.order_by(User.created_at.desc())
elif order_by == "last_active_at":
if direction == "asc":
query = query.order_by(User.last_active_at.asc())
else:
query = query.order_by(User.last_active_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(User.updated_at.asc())
else:
query = query.order_by(User.updated_at.desc())
elif order_by == "role":
if direction == "asc":
query = query.order_by(User.role.asc())
else:
query = query.order_by(User.role.desc())
else:
query = query.order_by(User.created_at.desc())
if skip:
query = query.offset(skip)
@@ -172,8 +231,10 @@ class UsersTable:
query = query.limit(limit)
users = query.all()
return [UserModel.model_validate(user) for user in users]
return {
"users": [UserModel.model_validate(user) for user in users],
"total": db.query(User).count(),
}
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
with get_db() as db:
@@ -330,5 +391,13 @@ class UsersTable:
users = db.query(User).filter(User.id.in_(user_ids)).all()
return [user.id for user in users]
def get_super_admin_user(self) -> Optional[UserModel]:
with get_db() as db:
user = db.query(User).filter_by(role="admin").first()
if user:
return UserModel.model_validate(user)
else:
return None
Users = UsersTable()
@@ -0,0 +1,53 @@
import requests
import logging
from typing import Iterator, List, Union
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalLoader(BaseLoader):
def __init__(
self,
web_paths: Union[str, List[str]],
external_url: str,
external_api_key: str,
continue_on_failure: bool = True,
**kwargs,
) -> None:
self.external_url = external_url
self.external_api_key = external_api_key
self.urls = web_paths if isinstance(web_paths, list) else [web_paths]
self.continue_on_failure = continue_on_failure
def lazy_load(self) -> Iterator[Document]:
batch_size = 20
for i in range(0, len(self.urls), batch_size):
urls = self.urls[i : i + batch_size]
try:
response = requests.post(
self.external_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {self.external_api_key}",
},
json={
"urls": urls,
},
)
response.raise_for_status()
results = response.json()
for result in results:
yield Document(
page_content=result.get("page_content", ""),
metadata=result.get("metadata", {}),
)
except Exception as e:
if self.continue_on_failure:
log.error(f"Error extracting content from batch {urls}: {e}")
else:
raise e
+95 -9
View File
@@ -20,6 +20,9 @@ from langchain_community.document_loaders import (
YoutubeLoader,
)
from langchain_core.documents import Document
from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
@@ -82,11 +85,13 @@ known_source_ext = [
class TikaLoader:
def __init__(self, url, file_path, mime_type=None):
def __init__(self, url, file_path, mime_type=None, extract_images=None):
self.url = url
self.file_path = file_path
self.mime_type = mime_type
self.extract_images = extract_images
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
@@ -96,6 +101,9 @@ class TikaLoader:
else:
headers = {}
if self.extract_images == True:
headers["X-Tika-PDFextractInlineImages"] = "true"
endpoint = self.url
if not endpoint.endswith("/"):
endpoint += "/"
@@ -105,7 +113,7 @@ class TikaLoader:
if r.ok:
raw_metadata = r.json()
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
text = raw_metadata.get("X-TIKA:content", "<No text content found>").strip()
if "Content-Type" in raw_metadata:
headers["Content-Type"] = raw_metadata["Content-Type"]
@@ -117,6 +125,62 @@ class TikaLoader:
raise Exception(f"Error calling Tika: {r.reason}")
class DoclingLoader:
def __init__(
self, url, file_path=None, mime_type=None, ocr_engine=None, ocr_lang=None
):
self.url = url.rstrip("/")
self.file_path = file_path
self.mime_type = mime_type
self.ocr_engine = ocr_engine
self.ocr_lang = ocr_lang
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
files = {
"files": (
self.file_path,
f,
self.mime_type or "application/octet-stream",
)
}
params = {
"image_export_mode": "placeholder",
"table_mode": "accurate",
}
if self.ocr_engine and self.ocr_lang:
params["ocr_engine"] = self.ocr_engine
params["ocr_lang"] = [
lang.strip() for lang in self.ocr_lang.split(",") if lang.strip()
]
endpoint = f"{self.url}/v1alpha/convert/file"
r = requests.post(endpoint, files=files, data=params)
if r.ok:
result = r.json()
document_data = result.get("document", {})
text = document_data.get("md_content", "<No text content found>")
metadata = {"Content-Type": self.mime_type} if self.mime_type else {}
log.debug("Docling extracted text: %s", text)
return [Document(page_content=text, metadata=metadata)]
else:
error_msg = f"Error calling Docling API: {r.reason}"
if r.text:
try:
error_data = r.json()
if "detail" in error_data:
error_msg += f" - {error_data['detail']}"
except Exception:
error_msg += f" - {r.text}"
raise Exception(f"Error calling Docling: {error_msg}")
class Loader:
def __init__(self, engine: str = "", **kwargs):
self.engine = engine
@@ -135,19 +199,34 @@ class Loader:
for doc in docs
]
def _is_text_file(self, file_ext: str, file_content_type: str) -> bool:
return file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
)
def _get_loader(self, filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower()
if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TikaLoader(
url=self.kwargs.get("TIKA_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
)
elif self.engine == "docling" and self.kwargs.get("DOCLING_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = DoclingLoader(
url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
ocr_engine=self.kwargs.get("DOCLING_OCR_ENGINE"),
ocr_lang=self.kwargs.get("DOCLING_OCR_LANG"),
)
elif (
self.engine == "document_intelligence"
@@ -170,13 +249,22 @@ class Loader:
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
)
elif (
self.engine == "mistral_ocr"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
and file_ext
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
)
else:
if file_ext == "pdf":
loader = PyPDFLoader(
file_path, extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES")
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
loader = CSVLoader(file_path, autodetect_encoding=True)
elif file_ext == "rst":
loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml":
@@ -205,9 +293,7 @@ class Loader:
loader = UnstructuredPowerPointLoader(file_path)
elif file_ext == "msg":
loader = OutlookMessageLoader(file_path)
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
elif self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
@@ -0,0 +1,225 @@
import requests
import logging
import os
import sys
from typing import List, Dict, Any
from langchain_core.documents import Document
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["RAG"])
class MistralLoader:
"""
Loads documents by processing them through the Mistral OCR API.
"""
BASE_API_URL = "https://api.mistral.ai/v1"
def __init__(self, api_key: str, file_path: str):
"""
Initializes the loader.
Args:
api_key: Your Mistral API key.
file_path: The local path to the PDF file to process.
"""
if not api_key:
raise ValueError("API key cannot be empty.")
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found at {file_path}")
self.api_key = api_key
self.file_path = file_path
self.headers = {"Authorization": f"Bearer {self.api_key}"}
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""Checks response status and returns JSON content."""
try:
response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
# Handle potential empty responses for certain successful requests (e.g., DELETE)
if response.status_code == 204 or not response.content:
return {} # Return empty dict if no content
return response.json()
except requests.exceptions.HTTPError as http_err:
log.error(f"HTTP error occurred: {http_err} - Response: {response.text}")
raise
except requests.exceptions.RequestException as req_err:
log.error(f"Request exception occurred: {req_err}")
raise
except ValueError as json_err: # Includes JSONDecodeError
log.error(f"JSON decode error: {json_err} - Response: {response.text}")
raise # Re-raise after logging
def _upload_file(self) -> str:
"""Uploads the file to Mistral for OCR processing."""
log.info("Uploading file to Mistral API")
url = f"{self.BASE_API_URL}/files"
file_name = os.path.basename(self.file_path)
try:
with open(self.file_path, "rb") as f:
files = {"file": (file_name, f, "application/pdf")}
data = {"purpose": "ocr"}
upload_headers = self.headers.copy() # Avoid modifying self.headers
response = requests.post(
url, headers=upload_headers, files=files, data=data
)
response_data = self._handle_response(response)
file_id = response_data.get("id")
if not file_id:
raise ValueError("File ID not found in upload response.")
log.info(f"File uploaded successfully. File ID: {file_id}")
return file_id
except Exception as e:
log.error(f"Failed to upload file: {e}")
raise
def _get_signed_url(self, file_id: str) -> str:
"""Retrieves a temporary signed URL for the uploaded file."""
log.info(f"Getting signed URL for file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}/url"
params = {"expiry": 1}
signed_url_headers = {**self.headers, "Accept": "application/json"}
try:
response = requests.get(url, headers=signed_url_headers, params=params)
response_data = self._handle_response(response)
signed_url = response_data.get("url")
if not signed_url:
raise ValueError("Signed URL not found in response.")
log.info("Signed URL received.")
return signed_url
except Exception as e:
log.error(f"Failed to get signed URL: {e}")
raise
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
"""Sends the signed URL to the OCR endpoint for processing."""
log.info("Processing OCR via Mistral API")
url = f"{self.BASE_API_URL}/ocr"
ocr_headers = {
**self.headers,
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {
"model": "mistral-ocr-latest",
"document": {
"type": "document_url",
"document_url": signed_url,
},
"include_image_base64": False,
}
try:
response = requests.post(url, headers=ocr_headers, json=payload)
ocr_response = self._handle_response(response)
log.info("OCR processing done.")
log.debug("OCR response: %s", ocr_response)
return ocr_response
except Exception as e:
log.error(f"Failed during OCR processing: {e}")
raise
def _delete_file(self, file_id: str) -> None:
"""Deletes the file from Mistral storage."""
log.info(f"Deleting uploaded file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}"
# No specific Accept header needed, default or Authorization is usually sufficient
try:
response = requests.delete(url, headers=self.headers)
delete_response = self._handle_response(
response
) # Check status, ignore response body unless needed
log.info(
f"File deleted successfully: {delete_response}"
) # Log the response if available
except Exception as e:
# Log error but don't necessarily halt execution if deletion fails
log.error(f"Failed to delete file ID {file_id}: {e}")
# Depending on requirements, you might choose to raise the error here
def load(self) -> List[Document]:
"""
Executes the full OCR workflow: upload, get URL, process OCR, delete file.
Returns:
A list of Document objects, one for each page processed.
"""
file_id = None
try:
# 1. Upload file
file_id = self._upload_file()
# 2. Get Signed URL
signed_url = self._get_signed_url(file_id)
# 3. Process OCR
ocr_response = self._process_ocr(signed_url)
# 4. Process results
pages_data = ocr_response.get("pages")
if not pages_data:
log.warning("No pages found in OCR response.")
return [Document(page_content="No text content found", metadata={})]
documents = []
total_pages = len(pages_data)
for page_data in pages_data:
page_content = page_data.get("markdown")
page_index = page_data.get("index") # API uses 0-based index
if page_content is not None and page_index is not None:
documents.append(
Document(
page_content=page_content,
metadata={
"page": page_index, # 0-based index from API
"page_label": page_index
+ 1, # 1-based label for convenience
"total_pages": total_pages,
# Add other relevant metadata from page_data if available/needed
# e.g., page_data.get('width'), page_data.get('height')
},
)
)
else:
log.warning(
f"Skipping page due to missing 'markdown' or 'index'. Data: {page_data}"
)
if not documents:
# Case where pages existed but none had valid markdown/index
log.warning(
"OCR response contained pages, but none had valid content/index."
)
return [
Document(
page_content="No text content found in valid pages", metadata={}
)
]
return documents
except Exception as e:
log.error(f"An error occurred during the loading process: {e}")
# Return an empty list or a specific error document on failure
return [Document(page_content=f"Error during processing: {e}", metadata={})]
finally:
# 5. Delete file (attempt even if prior steps failed after upload)
if file_id:
try:
self._delete_file(file_id)
except Exception as del_e:
# Log deletion error, but don't overwrite original error if one occurred
log.error(
f"Cleanup error: Could not delete file ID {file_id}. Reason: {del_e}"
)
@@ -0,0 +1,93 @@
import requests
import logging
from typing import Iterator, List, Literal, Union
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class TavilyLoader(BaseLoader):
"""Extract web page content from URLs using Tavily Extract API.
This is a LangChain document loader that uses Tavily's Extract API to
retrieve content from web pages and return it as Document objects.
Args:
urls: URL or list of URLs to extract content from.
api_key: The Tavily API key.
extract_depth: Depth of extraction, either "basic" or "advanced".
continue_on_failure: Whether to continue if extraction of a URL fails.
"""
def __init__(
self,
urls: Union[str, List[str]],
api_key: str,
extract_depth: Literal["basic", "advanced"] = "basic",
continue_on_failure: bool = True,
) -> None:
"""Initialize Tavily Extract client.
Args:
urls: URL or list of URLs to extract content from.
api_key: The Tavily API key.
include_images: Whether to include images in the extraction.
extract_depth: Depth of extraction, either "basic" or "advanced".
advanced extraction retrieves more data, including tables and
embedded content, with higher success but may increase latency.
basic costs 1 credit per 5 successful URL extractions,
advanced costs 2 credits per 5 successful URL extractions.
continue_on_failure: Whether to continue if extraction of a URL fails.
"""
if not urls:
raise ValueError("At least one URL must be provided.")
self.api_key = api_key
self.urls = urls if isinstance(urls, list) else [urls]
self.extract_depth = extract_depth
self.continue_on_failure = continue_on_failure
self.api_url = "https://api.tavily.com/extract"
def lazy_load(self) -> Iterator[Document]:
"""Extract and yield documents from the URLs using Tavily Extract API."""
batch_size = 20
for i in range(0, len(self.urls), batch_size):
batch_urls = self.urls[i : i + batch_size]
try:
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
# Use string for single URL, array for multiple URLs
urls_param = batch_urls[0] if len(batch_urls) == 1 else batch_urls
payload = {"urls": urls_param, "extract_depth": self.extract_depth}
# Make the API call
response = requests.post(self.api_url, headers=headers, json=payload)
response.raise_for_status()
response_data = response.json()
# Process successful results
for result in response_data.get("results", []):
url = result.get("url", "")
content = result.get("raw_content", "")
if not content:
log.warning(f"No content extracted from {url}")
continue
# Add URLs as metadata
metadata = {"source": url}
yield Document(
page_content=content,
metadata=metadata,
)
for failed in response_data.get("failed_results", []):
url = failed.get("url", "")
error = failed.get("error", "Unknown error")
log.error(f"Failed to extract content from {url}: {error}")
except Exception as e:
if self.continue_on_failure:
log.error(f"Error extracting content from batch {batch_urls}: {e}")
else:
raise e
+33 -14
View File
@@ -62,12 +62,17 @@ class YoutubeLoader:
_video_id = _parse_video_id(video_id)
self.video_id = _video_id if _video_id is not None else video_id
self._metadata = {"source": video_id}
self.language = language
self.proxy_url = proxy_url
# Ensure language is a list
if isinstance(language, str):
self.language = [language]
else:
self.language = language
self.language = list(language)
# Add English as fallback if not already in the list
if "en" not in self.language:
self.language.append("en")
def load(self) -> List[Document]:
"""Load YouTube transcripts into `Document` objects."""
@@ -101,17 +106,31 @@ class YoutubeLoader:
log.exception("Loading YouTube transcript failed")
return []
try:
transcript = transcript_list.find_transcript(self.language)
except NoTranscriptFound:
transcript = transcript_list.find_transcript(["en"])
# Try each language in order of priority
for lang in self.language:
try:
transcript = transcript_list.find_transcript([lang])
log.debug(f"Found transcript for language '{lang}'")
transcript_pieces: List[Dict[str, Any]] = transcript.fetch()
transcript_text = " ".join(
map(
lambda transcript_piece: transcript_piece.text.strip(" "),
transcript_pieces,
)
)
return [Document(page_content=transcript_text, metadata=self._metadata)]
except NoTranscriptFound:
log.debug(f"No transcript found for language '{lang}'")
continue
except Exception as e:
log.info(f"Error finding transcript for language '{lang}'")
raise e
transcript_pieces: List[Dict[str, Any]] = transcript.fetch()
transcript = " ".join(
map(
lambda transcript_piece: transcript_piece["text"].strip(" "),
transcript_pieces,
)
# If we get here, all languages failed
languages_tried = ", ".join(self.language)
log.warning(
f"No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed."
)
raise NoTranscriptFound(
f"No transcript found for any supported language. Verify if the video has transcripts, add more languages if needed."
)
return [Document(page_content=transcript, metadata=self._metadata)]
@@ -0,0 +1,58 @@
import logging
import requests
from typing import Optional, List, Tuple
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalReranker:
def __init__(
self,
api_key: str,
url: str = "http://localhost:8080/v1/rerank",
model: str = "reranker",
):
self.api_key = api_key
self.url = url
self.model = model
def predict(self, sentences: List[Tuple[str, str]]) -> Optional[List[float]]:
query = sentences[0][0]
docs = [i[1] for i in sentences]
payload = {
"model": self.model,
"query": query,
"documents": docs,
"top_n": len(docs),
}
try:
log.info(f"ExternalReranker:predict:model {self.model}")
log.info(f"ExternalReranker:predict:query {query}")
r = requests.post(
f"{self.url}",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
json=payload,
)
r.raise_for_status()
data = r.json()
if "results" in data:
sorted_results = sorted(data["results"], key=lambda x: x["index"])
return [result["relevance_score"] for result in sorted_results]
else:
log.error("No results found in external reranking response")
return None
except Exception as e:
log.exception(f"Error in external reranking: {e}")
return None
+224 -84
View File
@@ -1,30 +1,35 @@
import logging
import os
import uuid
from typing import Optional, Union
import asyncio
import requests
import hashlib
from concurrent.futures import ThreadPoolExecutor
from huggingface_hub import snapshot_download
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.misc import get_last_user_message, calculate_sha256_string
from open_webui.models.users import UserModel
from open_webui.models.files import Files
from open_webui.retrieval.vector.main import GetResult
from open_webui.env import (
SRC_LOG_LEVELS,
OFFLINE_MODE,
ENABLE_FORWARD_USER_INFO_HEADERS,
)
from open_webui.config import (
RAG_EMBEDDING_QUERY_PREFIX,
RAG_EMBEDDING_CONTENT_PREFIX,
RAG_EMBEDDING_PREFIX_FIELD_NAME,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -49,7 +54,7 @@ class VectorSearchRetriever(BaseRetriever):
) -> list[Document]:
result = VECTOR_DB_CLIENT.search(
collection_name=self.collection_name,
vectors=[self.embedding_function(query)],
vectors=[self.embedding_function(query, RAG_EMBEDDING_QUERY_PREFIX)],
limit=self.top_k,
)
@@ -72,6 +77,7 @@ def query_doc(
collection_name: str, query_embedding: list[float], k: int, user: UserModel = None
):
try:
log.debug(f"query_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.search(
collection_name=collection_name,
vectors=[query_embedding],
@@ -89,6 +95,7 @@ def query_doc(
def get_doc(collection_name: str, user: UserModel = None):
try:
log.debug(f"get_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
if result:
@@ -102,18 +109,19 @@ def get_doc(collection_name: str, user: UserModel = None):
def query_doc_with_hybrid_search(
collection_name: str,
collection_result: GetResult,
query: str,
embedding_function,
k: int,
reranking_function,
k_reranker: int,
r: float,
) -> dict:
try:
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
log.debug(f"query_doc_with_hybrid_search:doc {collection_name}")
bm25_retriever = BM25Retriever.from_texts(
texts=result.documents[0],
metadatas=result.metadatas[0],
texts=collection_result.documents[0],
metadatas=collection_result.metadatas[0],
)
bm25_retriever.k = k
@@ -128,7 +136,7 @@ def query_doc_with_hybrid_search(
)
compressor = RerankCompressor(
embedding_function=embedding_function,
top_n=k,
top_n=k_reranker,
reranking_function=reranking_function,
r_score=r,
)
@@ -138,10 +146,23 @@ def query_doc_with_hybrid_search(
)
result = compression_retriever.invoke(query)
distances = [d.metadata.get("score") for d in result]
documents = [d.page_content for d in result]
metadatas = [d.metadata for d in result]
# retrieve only min(k, k_reranker) items, sort and cut by distance if k < k_reranker
if k < k_reranker:
sorted_items = sorted(
zip(distances, metadatas, documents), key=lambda x: x[0], reverse=True
)
sorted_items = sorted_items[:k]
distances, documents, metadatas = map(list, zip(*sorted_items))
result = {
"distances": [[d.metadata.get("score") for d in result]],
"documents": [[d.page_content for d in result]],
"metadatas": [[d.metadata for d in result]],
"distances": [distances],
"documents": [documents],
"metadatas": [metadatas],
}
log.info(
@@ -150,6 +171,7 @@ def query_doc_with_hybrid_search(
)
return result
except Exception as e:
log.exception(f"Error querying doc {collection_name} with hybrid search: {e}")
raise e
@@ -174,12 +196,9 @@ def merge_get_results(get_results: list[dict]) -> dict:
return result
def merge_and_sort_query_results(
query_results: list[dict], k: int, reverse: bool = False
) -> dict:
def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict:
# Initialize lists to store combined data
combined = []
seen_hashes = set() # To store unique document hashes
combined = dict() # To store documents with unique document hashes
for data in query_results:
distances = data["distances"][0]
@@ -188,16 +207,21 @@ def merge_and_sort_query_results(
for distance, document, metadata in zip(distances, documents, metadatas):
if isinstance(document, str):
doc_hash = hashlib.md5(
doc_hash = hashlib.sha256(
document.encode()
).hexdigest() # Compute a hash for uniqueness
if doc_hash not in seen_hashes:
seen_hashes.add(doc_hash)
combined.append((distance, document, metadata))
if doc_hash not in combined.keys():
combined[doc_hash] = (distance, document, metadata)
continue # if doc is new, no further comparison is needed
# if doc is alredy in, but new distance is better, update
if distance > combined[doc_hash][0]:
combined[doc_hash] = (distance, document, metadata)
combined = list(combined.values())
# Sort the list based on distances
combined.sort(key=lambda x: x[0], reverse=reverse)
combined.sort(key=lambda x: x[0], reverse=True)
# Slice to keep only the top k elements
sorted_distances, sorted_documents, sorted_metadatas = (
@@ -236,29 +260,49 @@ def query_collection(
k: int,
) -> dict:
results = []
for query in queries:
query_embedding = embedding_function(query)
for collection_name in collection_names:
if collection_name:
try:
result = query_doc(
collection_name=collection_name,
k=k,
query_embedding=query_embedding,
)
if result is not None:
results.append(result.model_dump())
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
else:
pass
error = False
if VECTOR_DB == "chroma":
# Chroma uses unconventional cosine similarity, so we don't need to reverse the results
# https://docs.trychroma.com/docs/collections/configure#configuring-chroma-collections
return merge_and_sort_query_results(results, k=k, reverse=False)
else:
return merge_and_sort_query_results(results, k=k, reverse=True)
def process_query_collection(collection_name, query_embedding):
try:
if collection_name:
result = query_doc(
collection_name=collection_name,
k=k,
query_embedding=query_embedding,
)
if result is not None:
return result.model_dump(), None
return None, None
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
return None, e
# Generate all query embeddings (in one call)
query_embeddings = embedding_function(queries, prefix=RAG_EMBEDDING_QUERY_PREFIX)
log.debug(
f"query_collection: processing {len(queries)} queries across {len(collection_names)} collections"
)
with ThreadPoolExecutor() as executor:
future_results = []
for query_embedding in query_embeddings:
for collection_name in collection_names:
result = executor.submit(
process_query_collection, collection_name, query_embedding
)
future_results.append(result)
task_results = [future.result() for future in future_results]
for result, err in task_results:
if err is not None:
error = True
elif result is not None:
results.append(result)
if error and not results:
log.warning("All collection queries failed. No results returned.")
return merge_and_sort_query_results(results, k=k)
def query_collection_with_hybrid_search(
@@ -267,39 +311,72 @@ def query_collection_with_hybrid_search(
embedding_function,
k: int,
reranking_function,
k_reranker: int,
r: float,
) -> dict:
results = []
error = False
# Fetch collection data once per collection sequentially
# Avoid fetching the same data multiple times later
collection_results = {}
for collection_name in collection_names:
try:
for query in queries:
result = query_doc_with_hybrid_search(
collection_name=collection_name,
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
results.append(result)
except Exception as e:
log.exception(
"Error when querying the collection with " f"hybrid_search: {e}"
log.debug(
f"query_collection_with_hybrid_search:VECTOR_DB_CLIENT.get:collection {collection_name}"
)
error = True
collection_results[collection_name] = VECTOR_DB_CLIENT.get(
collection_name=collection_name
)
except Exception as e:
log.exception(f"Failed to fetch collection {collection_name}: {e}")
collection_results[collection_name] = None
if error:
log.info(
f"Starting hybrid search for {len(queries)} queries in {len(collection_names)} collections..."
)
def process_query(collection_name, query):
try:
result = query_doc_with_hybrid_search(
collection_name=collection_name,
collection_result=collection_results[collection_name],
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
k_reranker=k_reranker,
r=r,
)
return result, None
except Exception as e:
log.exception(f"Error when querying the collection with hybrid_search: {e}")
return None, e
# Prepare tasks for all collections and queries
# Avoid running any tasks for collections that failed to fetch data (have assigned None)
tasks = [
(cn, q)
for cn in collection_names
if collection_results[cn] is not None
for q in queries
]
with ThreadPoolExecutor() as executor:
future_results = [executor.submit(process_query, cn, q) for cn, q in tasks]
task_results = [future.result() for future in future_results]
for result, err in task_results:
if err is not None:
error = True
elif result is not None:
results.append(result)
if error and not results:
raise Exception(
"Hybrid search failed for all collections. Using Non hybrid search as fallback."
"Hybrid search failed for all collections. Using Non-hybrid search as fallback."
)
if VECTOR_DB == "chroma":
# Chroma uses unconventional cosine similarity, so we don't need to reverse the results
# https://docs.trychroma.com/docs/collections/configure#configuring-chroma-collections
return merge_and_sort_query_results(results, k=k, reverse=False)
else:
return merge_and_sort_query_results(results, k=k, reverse=True)
return merge_and_sort_query_results(results, k=k)
def get_embedding_function(
@@ -311,29 +388,38 @@ def get_embedding_function(
embedding_batch_size,
):
if embedding_engine == "":
return lambda query, user=None: embedding_function.encode(query).tolist()
return lambda query, prefix=None, user=None: embedding_function.encode(
query, **({"prompt": prefix} if prefix else {})
).tolist()
elif embedding_engine in ["ollama", "openai"]:
func = lambda query, user=None: generate_embeddings(
func = lambda query, prefix=None, user=None: generate_embeddings(
engine=embedding_engine,
model=embedding_model,
text=query,
prefix=prefix,
url=url,
key=key,
user=user,
)
def generate_multiple(query, user, func):
def generate_multiple(query, prefix, user, func):
if isinstance(query, list):
embeddings = []
for i in range(0, len(query), embedding_batch_size):
embeddings.extend(
func(query[i : i + embedding_batch_size], user=user)
func(
query[i : i + embedding_batch_size],
prefix=prefix,
user=user,
)
)
return embeddings
else:
return func(query, user)
return func(query, prefix, user)
return lambda query, user=None: generate_multiple(query, user, func)
return lambda query, prefix=None, user=None: generate_multiple(
query, prefix, user, func
)
else:
raise ValueError(f"Unknown embedding engine: {embedding_engine}")
@@ -345,6 +431,7 @@ def get_sources_from_files(
embedding_function,
k,
reranking_function,
k_reranker,
r,
hybrid_search,
full_context=False,
@@ -414,6 +501,13 @@ def get_sources_from_files(
]
],
}
elif file.get("file").get("data"):
context = {
"documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [
[file.get("file").get("data", {}).get("metadata", {})]
],
}
else:
collection_names = []
if file.get("type") == "collection":
@@ -454,6 +548,7 @@ def get_sources_from_files(
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
k_reranker=k_reranker,
r=r,
)
except Exception as e:
@@ -546,9 +641,17 @@ def generate_openai_batch_embeddings(
texts: list[str],
url: str = "https://api.openai.com/v1",
key: str = "",
prefix: str = None,
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_openai_batch_embeddings:model {model} batch size: {len(texts)}"
)
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
r = requests.post(
f"{url}/embeddings",
headers={
@@ -565,7 +668,7 @@ def generate_openai_batch_embeddings(
else {}
),
},
json={"input": texts, "model": model},
json=json_data,
)
r.raise_for_status()
data = r.json()
@@ -579,9 +682,21 @@ def generate_openai_batch_embeddings(
def generate_ollama_batch_embeddings(
model: str, texts: list[str], url: str, key: str = "", user: UserModel = None
model: str,
texts: list[str],
url: str,
key: str = "",
prefix: str = None,
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}"
)
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
r = requests.post(
f"{url}/api/embed",
headers={
@@ -598,7 +713,7 @@ def generate_ollama_batch_embeddings(
else {}
),
},
json={"input": texts, "model": model},
json=json_data,
)
r.raise_for_status()
data = r.json()
@@ -612,15 +727,34 @@ def generate_ollama_batch_embeddings(
return None
def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **kwargs):
def generate_embeddings(
engine: str,
model: str,
text: Union[str, list[str]],
prefix: Union[str, None] = None,
**kwargs,
):
url = kwargs.get("url", "")
key = kwargs.get("key", "")
user = kwargs.get("user")
if prefix is not None and RAG_EMBEDDING_PREFIX_FIELD_NAME is None:
if isinstance(text, list):
text = [f"{prefix}{text_element}" for text_element in text]
else:
text = f"{prefix}{text}"
if engine == "ollama":
if isinstance(text, list):
embeddings = generate_ollama_batch_embeddings(
**{"model": model, "texts": text, "url": url, "key": key, "user": user}
**{
"model": model,
"texts": text,
"url": url,
"key": key,
"prefix": prefix,
"user": user,
}
)
else:
embeddings = generate_ollama_batch_embeddings(
@@ -629,16 +763,20 @@ def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **
"texts": [text],
"url": url,
"key": key,
"prefix": prefix,
"user": user,
}
)
return embeddings[0] if isinstance(text, str) else embeddings
elif engine == "openai":
if isinstance(text, list):
embeddings = generate_openai_batch_embeddings(model, text, url, key, user)
embeddings = generate_openai_batch_embeddings(
model, text, url, key, prefix, user
)
else:
embeddings = generate_openai_batch_embeddings(model, [text], url, key, user)
embeddings = generate_openai_batch_embeddings(
model, [text], url, key, prefix, user
)
return embeddings[0] if isinstance(text, str) else embeddings
@@ -674,13 +812,15 @@ class RerankCompressor(BaseDocumentCompressor):
else:
from sentence_transformers import util
query_embedding = self.embedding_function(query)
query_embedding = self.embedding_function(query, RAG_EMBEDDING_QUERY_PREFIX)
document_embedding = self.embedding_function(
[doc.page_content for doc in documents]
[doc.page_content for doc in documents], RAG_EMBEDDING_CONTENT_PREFIX
)
scores = util.cos_sim(query_embedding, document_embedding)[0]
docs_with_scores = list(zip(documents, scores.tolist()))
docs_with_scores = list(
zip(documents, scores.tolist() if not isinstance(scores, list) else scores)
)
if self.r_score:
docs_with_scores = [
(d, s) for d, s in docs_with_scores if s >= self.r_score
@@ -16,6 +16,14 @@ elif VECTOR_DB == "pgvector":
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
elif VECTOR_DB == "elasticsearch":
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
VECTOR_DB_CLIENT = ElasticsearchClient()
elif VECTOR_DB == "pinecone":
from open_webui.retrieval.vector.dbs.pinecone import PineconeClient
VECTOR_DB_CLIENT = PineconeClient()
else:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
@@ -5,7 +5,12 @@ from chromadb.utils.batch_utils import create_batches
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
CHROMA_DATA_PATH,
CHROMA_HTTP_HOST,
@@ -23,7 +28,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ChromaClient:
class ChromaClient(VectorDBBase):
def __init__(self):
settings_dict = {
"allow_reset": True,
@@ -75,10 +80,16 @@ class ChromaClient:
n_results=limit,
)
# chromadb has cosine distance, 2 (worst) -> 0 (best). Re-odering to 0 -> 1
# https://docs.trychroma.com/docs/collections/configure cosine equation
distances: list = result["distances"][0]
distances = [2 - dist for dist in distances]
distances = [[dist / 2 for dist in distances]]
return SearchResult(
**{
"ids": result["ids"],
"distances": result["distances"],
"distances": distances,
"documents": result["documents"],
"metadatas": result["metadatas"],
}
@@ -166,12 +177,19 @@ class ChromaClient:
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
collection = self.client.get_collection(name=collection_name)
if collection:
if ids:
collection.delete(ids=ids)
elif filter:
collection.delete(where=filter)
try:
collection = self.client.get_collection(name=collection_name)
if collection:
if ids:
collection.delete(ids=ids)
elif filter:
collection.delete(where=filter)
except Exception as e:
# If collection doesn't exist, that's fine - nothing to delete
log.debug(
f"Attempted to delete from non-existent collection {collection_name}. Ignoring."
)
pass
def reset(self):
# Resets the database. This will delete all collections and item entries.
@@ -0,0 +1,300 @@
from elasticsearch import Elasticsearch, BadRequestError
from typing import Optional
import ssl
from elasticsearch.helpers import bulk, scan
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
ELASTICSEARCH_URL,
ELASTICSEARCH_CA_CERTS,
ELASTICSEARCH_API_KEY,
ELASTICSEARCH_USERNAME,
ELASTICSEARCH_PASSWORD,
ELASTICSEARCH_CLOUD_ID,
ELASTICSEARCH_INDEX_PREFIX,
SSL_ASSERT_FINGERPRINT,
)
class ElasticsearchClient(VectorDBBase):
"""
Important:
in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating
an index for each file but store it as a text field, while seperating to different index
baesd on the embedding length.
"""
def __init__(self):
self.index_prefix = ELASTICSEARCH_INDEX_PREFIX
self.client = Elasticsearch(
hosts=[ELASTICSEARCH_URL],
ca_certs=ELASTICSEARCH_CA_CERTS,
api_key=ELASTICSEARCH_API_KEY,
cloud_id=ELASTICSEARCH_CLOUD_ID,
basic_auth=(
(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD
else None
),
ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT,
)
# Status: works
def _get_index_name(self, dimension: int) -> str:
return f"{self.index_prefix}_d{str(dimension)}"
# Status: works
def _scan_result_to_get_result(self, result) -> GetResult:
if not result:
return None
ids = []
documents = []
metadatas = []
for hit in result:
ids.append(hit["_id"])
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
# Status: works
def _result_to_get_result(self, result) -> GetResult:
if not result["hits"]["hits"]:
return None
ids = []
documents = []
metadatas = []
for hit in result["hits"]["hits"]:
ids.append(hit["_id"])
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
# Status: works
def _result_to_search_result(self, result) -> SearchResult:
ids = []
distances = []
documents = []
metadatas = []
for hit in result["hits"]["hits"]:
ids.append(hit["_id"])
distances.append(hit["_score"])
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return SearchResult(
ids=[ids],
distances=[distances],
documents=[documents],
metadatas=[metadatas],
)
# Status: works
def _create_index(self, dimension: int):
body = {
"mappings": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {"type": "keyword"},
}
}
],
"properties": {
"collection": {"type": "keyword"},
"id": {"type": "keyword"},
"vector": {
"type": "dense_vector",
"dims": dimension, # Adjust based on your vector dimensions
"index": True,
"similarity": "cosine",
},
"text": {"type": "text"},
"metadata": {"type": "object"},
},
}
}
self.client.indices.create(index=self._get_index_name(dimension), body=body)
# Status: works
def _create_batches(self, items: list[VectorItem], batch_size=100):
for i in range(0, len(items), batch_size):
yield items[i : min(i + batch_size, len(items))]
# Status: works
def has_collection(self, collection_name) -> bool:
query_body = {"query": {"bool": {"filter": []}}}
query_body["query"]["bool"]["filter"].append(
{"term": {"collection": collection_name}}
)
try:
result = self.client.count(index=f"{self.index_prefix}*", body=query_body)
return result.body["count"] > 0
except Exception as e:
return None
def delete_collection(self, collection_name: str):
query = {"query": {"term": {"collection": collection_name}}}
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
# Status: works
def search(
self, collection_name: str, vectors: list[list[float]], limit: int
) -> Optional[SearchResult]:
query = {
"size": limit,
"_source": ["text", "metadata"],
"query": {
"script_score": {
"query": {
"bool": {"filter": [{"term": {"collection": collection_name}}]}
},
"script": {
"source": "cosineSimilarity(params.vector, 'vector') + 1.0",
"params": {
"vector": vectors[0]
}, # Assuming single query vector
},
}
},
}
result = self.client.search(
index=self._get_index_name(len(vectors[0])), body=query
)
return self._result_to_search_result(result)
# Status: only tested halfwat
def query(
self, collection_name: str, filter: dict, limit: Optional[int] = None
) -> Optional[GetResult]:
if not self.has_collection(collection_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}})
query_body["query"]["bool"]["filter"].append(
{"term": {"collection": collection_name}}
)
size = limit if limit else 10
try:
result = self.client.search(
index=f"{self.index_prefix}*",
body=query_body,
size=size,
)
return self._result_to_get_result(result)
except Exception as e:
return None
# Status: works
def _has_index(self, dimension: int):
return self.client.indices.exists(
index=self._get_index_name(dimension=dimension)
)
def get_or_create_index(self, dimension: int):
if not self._has_index(dimension=dimension):
self._create_index(dimension=dimension)
# Status: works
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
query = {
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}},
"_source": ["text", "metadata"],
}
results = list(scan(self.client, index=f"{self.index_prefix}*", query=query))
return self._scan_result_to_get_result(results)
# Status: works
def insert(self, collection_name: str, items: list[VectorItem]):
if not self._has_index(dimension=len(items[0]["vector"])):
self._create_index(dimension=len(items[0]["vector"]))
for batch in self._create_batches(items):
actions = [
{
"_index": self._get_index_name(dimension=len(items[0]["vector"])),
"_id": item["id"],
"_source": {
"collection": collection_name,
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
for item in batch
]
bulk(self.client, actions)
# Upsert documents using the update API with doc_as_upsert=True.
def upsert(self, collection_name: str, items: list[VectorItem]):
if not self._has_index(dimension=len(items[0]["vector"])):
self._create_index(dimension=len(items[0]["vector"]))
for batch in self._create_batches(items):
actions = [
{
"_op_type": "update",
"_index": self._get_index_name(dimension=len(item["vector"])),
"_id": item["id"],
"doc": {
"collection": collection_name,
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
"doc_as_upsert": True,
}
for item in batch
]
bulk(self.client, actions)
# Delete specific documents from a collection by filtering on both collection and document IDs.
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
query = {
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}}
}
# logic based on chromaDB
if ids:
query["query"]["bool"]["filter"].append({"terms": {"_id": ids}})
elif filter:
for field, value in filter.items():
query["query"]["bool"]["filter"].append(
{"term": {f"metadata.{field}": value}}
)
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
def reset(self):
indices = self.client.indices.get(index=f"{self.index_prefix}*")
for index in indices:
self.client.indices.delete(index=index)
+159 -43
View File
@@ -3,12 +3,21 @@ from pymilvus import FieldSchema, DataType
import json
import logging
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
MILVUS_URI,
MILVUS_DB,
MILVUS_TOKEN,
MILVUS_INDEX_TYPE,
MILVUS_METRIC_TYPE,
MILVUS_HNSW_M,
MILVUS_HNSW_EFCONSTRUCTION,
MILVUS_IVF_FLAT_NLIST,
)
from open_webui.env import SRC_LOG_LEVELS
@@ -16,19 +25,18 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MilvusClient:
class MilvusClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open_webui"
if MILVUS_TOKEN is None:
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB)
else:
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB, token=MILVUS_TOKEN)
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN)
def _result_to_get_result(self, result) -> GetResult:
ids = []
documents = []
metadatas = []
for match in result:
_ids = []
_documents = []
@@ -37,11 +45,9 @@ class MilvusClient:
_ids.append(item.get("id"))
_documents.append(item.get("data", {}).get("text"))
_metadatas.append(item.get("metadata"))
ids.append(_ids)
documents.append(_documents)
metadatas.append(_metadatas)
return GetResult(
**{
"ids": ids,
@@ -55,24 +61,23 @@ class MilvusClient:
distances = []
documents = []
metadatas = []
for match in result:
_ids = []
_distances = []
_documents = []
_metadatas = []
for item in match:
_ids.append(item.get("id"))
_distances.append(item.get("distance"))
# normalize milvus score from [-1, 1] to [0, 1] range
# https://milvus.io/docs/de/metric.md
_dist = (item.get("distance") + 1.0) / 2.0
_distances.append(_dist)
_documents.append(item.get("entity", {}).get("data", {}).get("text"))
_metadatas.append(item.get("entity", {}).get("metadata"))
ids.append(_ids)
distances.append(_distances)
documents.append(_documents)
metadatas.append(_metadatas)
return SearchResult(
**{
"ids": ids,
@@ -105,11 +110,39 @@ class MilvusClient:
)
index_params = self.client.prepare_index_params()
# Use configurations from config.py
index_type = MILVUS_INDEX_TYPE.upper()
metric_type = MILVUS_METRIC_TYPE.upper()
log.info(f"Using Milvus index type: {index_type}, metric type: {metric_type}")
index_creation_params = {}
if index_type == "HNSW":
index_creation_params = {
"M": MILVUS_HNSW_M,
"efConstruction": MILVUS_HNSW_EFCONSTRUCTION,
}
log.info(f"HNSW params: {index_creation_params}")
elif index_type == "IVF_FLAT":
index_creation_params = {"nlist": MILVUS_IVF_FLAT_NLIST}
log.info(f"IVF_FLAT params: {index_creation_params}")
elif index_type in ["FLAT", "AUTOINDEX"]:
log.info(f"Using {index_type} index with no specific build-time params.")
else:
log.warning(
f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. "
f"Supported types: HNSW, IVF_FLAT, FLAT, AUTOINDEX. "
f"Milvus will use its default for the collection if this type is not directly supported for index creation."
)
# For unsupported types, pass the type directly to Milvus; it might handle it or use a default.
# If Milvus errors out, the user needs to correct the MILVUS_INDEX_TYPE env var.
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 100},
index_type=index_type,
metric_type=metric_type,
params=index_creation_params,
)
self.client.create_collection(
@@ -117,6 +150,9 @@ class MilvusClient:
schema=schema,
index_params=index_params,
)
log.info(
f"Successfully created collection '{self.collection_prefix}_{collection_name}' with index type '{index_type}' and metric '{metric_type}'."
)
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
@@ -137,84 +173,113 @@ class MilvusClient:
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
collection_name = collection_name.replace("-", "_")
# For some index types like IVF_FLAT, search params like nprobe can be set.
# Example: search_params = {"nprobe": 10} if using IVF_FLAT
# For simplicity, not adding configurable search_params here, but could be extended.
result = self.client.search(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=vectors,
limit=limit,
output_fields=["data", "metadata"],
# search_params=search_params # Potentially add later if needed
)
return self._result_to_search_result(result)
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
# Construct the filter string for querying
collection_name = collection_name.replace("-", "_")
if not self.has_collection(collection_name):
log.warning(
f"Query attempted on non-existent collection: {self.collection_prefix}_{collection_name}"
)
return None
filter_string = " && ".join(
[
f'metadata["{key}"] == {json.dumps(value)}'
for key, value in filter.items()
]
)
max_limit = 16383 # The maximum number of records per request
all_results = []
if limit is None:
limit = float("inf") # Use infinity as a placeholder for no limit
# Milvus default limit for query if not specified is 16384, but docs mention iteration.
# Let's set a practical high number if "all" is intended, or handle true pagination.
# For now, if limit is None, we'll fetch in batches up to a very large number.
# This part could be refined based on expected use cases for "get all".
# For this function signature, None implies "as many as possible" up to Milvus limits.
limit = (
16384 * 10
) # A large number to signify fetching many, will be capped by actual data or max_limit per call.
log.info(
f"Limit not specified for query, fetching up to {limit} results in batches."
)
# Initialize offset and remaining to handle pagination
offset = 0
remaining = limit
try:
log.info(
f"Querying collection {self.collection_prefix}_{collection_name} with filter: '{filter_string}', limit: {limit}"
)
# Loop until there are no more items to fetch or the desired limit is reached
while remaining > 0:
log.info(f"remaining: {remaining}")
current_fetch = min(
max_limit, remaining
) # Determine how many items to fetch in this iteration
max_limit, remaining if isinstance(remaining, int) else max_limit
)
log.debug(
f"Querying with offset: {offset}, current_fetch: {current_fetch}"
)
results = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
output_fields=["*"],
output_fields=[
"id",
"data",
"metadata",
], # Explicitly list needed fields. Vector not usually needed in query.
limit=current_fetch,
offset=offset,
)
if not results:
log.debug("No more results from query.")
break
all_results.extend(results)
results_count = len(results)
remaining -= (
results_count # Decrease remaining by the number of items fetched
)
log.debug(f"Fetched {results_count} results in this batch.")
if isinstance(remaining, int):
remaining -= results_count
offset += results_count
# Break the loop if the results returned are less than the requested fetch count
# Break the loop if the results returned are less than the requested fetch count (means end of data)
if results_count < current_fetch:
log.debug(
"Fetched less than requested, assuming end of results for this query."
)
break
log.debug(all_results)
log.info(f"Total results from query: {len(all_results)}")
return self._result_to_get_result([all_results])
except Exception as e:
log.exception(
f"Error querying collection {collection_name} with limit {limit}: {e}"
f"Error querying collection {self.collection_prefix}_{collection_name} with filter '{filter_string}' and limit {limit}: {e}"
)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
# Get all the items in the collection. This can be very resource-intensive for large collections.
collection_name = collection_name.replace("-", "_")
result = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter='id != ""',
log.warning(
f"Fetching ALL items from collection '{self.collection_prefix}_{collection_name}'. This might be slow for large collections."
)
return self._result_to_get_result([result])
# Using query with a trivial filter to get all items.
# This will use the paginated query logic.
return self.query(collection_name=collection_name, filter={}, limit=None)
def insert(self, collection_name: str, items: list[VectorItem]):
# Insert the items into the collection, if the collection does not exist, it will be created.
@@ -222,10 +287,23 @@ class MilvusClient:
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
log.info(
f"Collection {self.collection_prefix}_{collection_name} does not exist. Creating now."
)
if not items:
log.error(
f"Cannot create collection {self.collection_prefix}_{collection_name} without items to determine dimension."
)
raise ValueError(
"Cannot create Milvus collection without items to determine vector dimension."
)
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
log.info(
f"Inserting {len(items)} items into collection {self.collection_prefix}_{collection_name}."
)
return self.client.insert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
@@ -245,10 +323,23 @@ class MilvusClient:
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
log.info(
f"Collection {self.collection_prefix}_{collection_name} does not exist for upsert. Creating now."
)
if not items:
log.error(
f"Cannot create collection {self.collection_prefix}_{collection_name} for upsert without items to determine dimension."
)
raise ValueError(
"Cannot create Milvus collection for upsert without items to determine vector dimension."
)
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
log.info(
f"Upserting {len(items)} items into collection {self.collection_prefix}_{collection_name}."
)
return self.client.upsert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
@@ -268,30 +359,55 @@ class MilvusClient:
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
# Delete the items from the collection based on the ids or filter.
collection_name = collection_name.replace("-", "_")
if not self.has_collection(collection_name):
log.warning(
f"Delete attempted on non-existent collection: {self.collection_prefix}_{collection_name}"
)
return None
if ids:
log.info(
f"Deleting items by IDs from {self.collection_prefix}_{collection_name}. IDs: {ids}"
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
ids=ids,
)
elif filter:
# Convert the filter dictionary to a string using JSON_CONTAINS.
filter_string = " && ".join(
[
f'metadata["{key}"] == {json.dumps(value)}'
for key, value in filter.items()
]
)
log.info(
f"Deleting items by filter from {self.collection_prefix}_{collection_name}. Filter: {filter_string}"
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
)
else:
log.warning(
f"Delete operation on {self.collection_prefix}_{collection_name} called without IDs or filter. No action taken."
)
return None
def reset(self):
# Resets the database. This will delete all collections and item entries.
# Resets the database. This will delete all collections and item entries that match the prefix.
log.warning(
f"Resetting Milvus: Deleting all collections with prefix '{self.collection_prefix}'."
)
collection_names = self.client.list_collections()
for collection_name in collection_names:
if collection_name.startswith(self.collection_prefix):
self.client.drop_collection(collection_name=collection_name)
deleted_collections = []
for collection_name_full in collection_names:
if collection_name_full.startswith(self.collection_prefix):
try:
self.client.drop_collection(collection_name=collection_name_full)
deleted_collections.append(collection_name_full)
log.info(f"Deleted collection: {collection_name_full}")
except Exception as e:
log.error(f"Error deleting collection {collection_name_full}: {e}")
log.info(f"Milvus reset complete. Deleted collections: {deleted_collections}")
@@ -1,7 +1,13 @@
from opensearchpy import OpenSearch
from opensearchpy.helpers import bulk
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
OPENSEARCH_URI,
OPENSEARCH_SSL,
@@ -11,7 +17,7 @@ from open_webui.config import (
)
class OpenSearchClient:
class OpenSearchClient(VectorDBBase):
def __init__(self):
self.index_prefix = "open_webui"
self.client = OpenSearch(
@@ -21,7 +27,13 @@ class OpenSearchClient:
http_auth=(OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD),
)
def _get_index_name(self, collection_name: str) -> str:
return f"{self.index_prefix}_{collection_name}"
def _result_to_get_result(self, result) -> GetResult:
if not result["hits"]["hits"]:
return None
ids = []
documents = []
metadatas = []
@@ -31,9 +43,12 @@ class OpenSearchClient:
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
def _result_to_search_result(self, result) -> SearchResult:
if not result["hits"]["hits"]:
return None
ids = []
distances = []
documents = []
@@ -46,72 +61,88 @@ class OpenSearchClient:
metadatas.append(hit["_source"].get("metadata"))
return SearchResult(
ids=ids, distances=distances, documents=documents, metadatas=metadatas
ids=[ids],
distances=[distances],
documents=[documents],
metadatas=[metadatas],
)
def _create_index(self, index_name: str, dimension: int):
def _create_index(self, collection_name: str, dimension: int):
body = {
"settings": {"index": {"knn": True}},
"mappings": {
"properties": {
"id": {"type": "keyword"},
"vector": {
"type": "dense_vector",
"dims": dimension, # Adjust based on your vector dimensions
"index": true,
"type": "knn_vector",
"dimension": dimension, # Adjust based on your vector dimensions
"index": True,
"similarity": "faiss",
"method": {
"name": "hnsw",
"space_type": "ip", # Use inner product to approximate cosine similarity
"space_type": "innerproduct", # Use inner product to approximate cosine similarity
"engine": "faiss",
"ef_construction": 128,
"m": 16,
"parameters": {
"ef_construction": 128,
"m": 16,
},
},
},
"text": {"type": "text"},
"metadata": {"type": "object"},
}
}
},
}
self.client.indices.create(index=f"{self.index_prefix}_{index_name}", body=body)
self.client.indices.create(
index=self._get_index_name(collection_name), body=body
)
def _create_batches(self, items: list[VectorItem], batch_size=100):
for i in range(0, len(items), batch_size):
yield items[i : i + batch_size]
def has_collection(self, index_name: str) -> bool:
def has_collection(self, collection_name: str) -> bool:
# has_collection here means has index.
# We are simply adapting to the norms of the other DBs.
return self.client.indices.exists(index=f"{self.index_prefix}_{index_name}")
return self.client.indices.exists(index=self._get_index_name(collection_name))
def delete_colleciton(self, index_name: str):
def delete_collection(self, collection_name: str):
# delete_collection here means delete index.
# We are simply adapting to the norms of the other DBs.
self.client.indices.delete(index=f"{self.index_prefix}_{index_name}")
self.client.indices.delete(index=self._get_index_name(collection_name))
def search(
self, index_name: str, vectors: list[list[float]], limit: int
self, collection_name: str, vectors: list[list[float | int]], limit: int
) -> Optional[SearchResult]:
query = {
"size": limit,
"_source": ["text", "metadata"],
"query": {
"script_score": {
"query": {"match_all": {}},
"script": {
"source": "cosineSimilarity(params.vector, 'vector') + 1.0",
"params": {
"vector": vectors[0]
}, # Assuming single query vector
},
}
},
}
try:
if not self.has_collection(collection_name):
return None
result = self.client.search(
index=f"{self.index_prefix}_{index_name}", body=query
)
query = {
"size": limit,
"_source": ["text", "metadata"],
"query": {
"script_score": {
"query": {"match_all": {}},
"script": {
"source": "(cosineSimilarity(params.query_value, doc[params.field]) + 1.0) / 2.0",
"params": {
"field": "vector",
"query_value": vectors[0],
}, # Assuming single query vector
},
}
},
}
return self._result_to_search_result(result)
result = self.client.search(
index=self._get_index_name(collection_name), body=query
)
return self._result_to_search_result(result)
except Exception as e:
return None
def query(
self, collection_name: str, filter: dict, limit: Optional[int] = None
@@ -125,13 +156,15 @@ class OpenSearchClient:
}
for field, value in filter.items():
query_body["query"]["bool"]["filter"].append({"term": {field: value}})
query_body["query"]["bool"]["filter"].append(
{"match": {"metadata." + str(field): value}}
)
size = limit if limit else 10
try:
result = self.client.search(
index=f"{self.index_prefix}_{collection_name}",
index=self._get_index_name(collection_name),
body=query_body,
size=size,
)
@@ -141,64 +174,88 @@ class OpenSearchClient:
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)
def _create_index_if_not_exists(self, collection_name: str, dimension: int):
if not self.has_collection(collection_name):
self._create_index(collection_name, dimension)
def get(self, index_name: str) -> Optional[GetResult]:
def get(self, collection_name: str) -> Optional[GetResult]:
query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]}
result = self.client.search(
index=f"{self.index_prefix}_{index_name}", body=query
index=self._get_index_name(collection_name), body=query
)
return self._result_to_get_result(result)
def insert(self, index_name: str, items: list[VectorItem]):
if not self.has_index(index_name):
self._create_index(index_name, dimension=len(items[0]["vector"]))
def insert(self, collection_name: str, items: list[VectorItem]):
self._create_index_if_not_exists(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
for batch in self._create_batches(items):
actions = [
{
"index": {
"_id": item["id"],
"_source": {
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
"_op_type": "index",
"_index": self._get_index_name(collection_name),
"_id": item["id"],
"_source": {
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
for item in batch
]
self.client.bulk(actions)
bulk(self.client, actions)
def upsert(self, index_name: str, items: list[VectorItem]):
if not self.has_index(index_name):
self._create_index(index_name, dimension=len(items[0]["vector"]))
def upsert(self, collection_name: str, items: list[VectorItem]):
self._create_index_if_not_exists(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
for batch in self._create_batches(items):
actions = [
{
"index": {
"_id": item["id"],
"_source": {
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
"_op_type": "update",
"_index": self._get_index_name(collection_name),
"_id": item["id"],
"doc": {
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
"doc_as_upsert": True,
}
for item in batch
]
self.client.bulk(actions)
bulk(self.client, actions)
def delete(self, index_name: str, ids: list[str]):
actions = [
{"delete": {"_index": f"{self.index_prefix}_{index_name}", "_id": id}}
for id in ids
]
self.client.bulk(body=actions)
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
if ids:
actions = [
{
"_op_type": "delete",
"_index": self._get_index_name(collection_name),
"_id": id,
}
for id in ids
]
bulk(self.client, actions)
elif filter:
query_body = {
"query": {"bool": {"filter": []}},
}
for field, value in filter.items():
query_body["query"]["bool"]["filter"].append(
{"match": {"metadata." + str(field): value}}
)
self.client.delete_by_query(
index=self._get_index_name(collection_name), body=query_body
)
def reset(self):
indices = self.client.indices.get(index=f"{self.index_prefix}_*")
@@ -22,7 +22,12 @@ from pgvector.sqlalchemy import Vector
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.exc import NoSuchTableError
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
from open_webui.env import SRC_LOG_LEVELS
@@ -44,7 +49,7 @@ class DocumentChunk(Base):
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
class PgvectorClient:
class PgvectorClient(VectorDBBase):
def __init__(self) -> None:
# if no pgvector uri, use the existing database connection
@@ -136,9 +141,8 @@ class PgvectorClient:
# Pad the vector with zeros
vector += [0.0] * (VECTOR_LENGTH - current_length)
elif current_length > VECTOR_LENGTH:
raise Exception(
f"Vector length {current_length} not supported. Max length must be <= {VECTOR_LENGTH}"
)
# Truncate the vector to VECTOR_LENGTH
vector = vector[:VECTOR_LENGTH]
return vector
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
@@ -278,7 +282,9 @@ class PgvectorClient:
for row in results:
qid = int(row.qid)
ids[qid].append(row.id)
distances[qid].append(row.distance)
# normalize and re-orders pgvec distance from [2, 0] to [0, 1] score range
# https://github.com/pgvector/pgvector?tab=readme-ov-file#querying
distances[qid].append((2.0 - row.distance) / 2.0)
documents[qid].append(row.text)
metadatas[qid].append(row.vmetadata)
@@ -0,0 +1,533 @@
from typing import Optional, List, Dict, Any, Union
import logging
import time # for measuring elapsed time
from pinecone import ServerlessSpec
import asyncio # for async upserts
import functools # for partial binding in async tasks
import concurrent.futures # for parallel batch upserts
from pinecone.grpc import PineconeGRPC # use gRPC client for faster upserts
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
PINECONE_API_KEY,
PINECONE_ENVIRONMENT,
PINECONE_INDEX_NAME,
PINECONE_DIMENSION,
PINECONE_METRIC,
PINECONE_CLOUD,
)
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
BATCH_SIZE = 100 # Recommended batch size for Pinecone operations
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class PineconeClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
# Validate required configuration
self._validate_config()
# Store configuration values
self.api_key = PINECONE_API_KEY
self.environment = PINECONE_ENVIRONMENT
self.index_name = PINECONE_INDEX_NAME
self.dimension = PINECONE_DIMENSION
self.metric = PINECONE_METRIC
self.cloud = PINECONE_CLOUD
# Initialize Pinecone gRPC client for improved performance
self.client = PineconeGRPC(
api_key=self.api_key, environment=self.environment, cloud=self.cloud
)
# Persistent executor for batch operations
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
# Create index if it doesn't exist
self._initialize_index()
def _validate_config(self) -> None:
"""Validate that all required configuration variables are set."""
missing_vars = []
if not PINECONE_API_KEY:
missing_vars.append("PINECONE_API_KEY")
if not PINECONE_ENVIRONMENT:
missing_vars.append("PINECONE_ENVIRONMENT")
if not PINECONE_INDEX_NAME:
missing_vars.append("PINECONE_INDEX_NAME")
if not PINECONE_DIMENSION:
missing_vars.append("PINECONE_DIMENSION")
if not PINECONE_CLOUD:
missing_vars.append("PINECONE_CLOUD")
if missing_vars:
raise ValueError(
f"Required configuration missing: {', '.join(missing_vars)}"
)
def _initialize_index(self) -> None:
"""Initialize the Pinecone index."""
try:
# Check if index exists
if self.index_name not in self.client.list_indexes().names():
log.info(f"Creating Pinecone index '{self.index_name}'...")
self.client.create_index(
name=self.index_name,
dimension=self.dimension,
metric=self.metric,
spec=ServerlessSpec(cloud=self.cloud, region=self.environment),
)
log.info(f"Successfully created Pinecone index '{self.index_name}'")
else:
log.info(f"Using existing Pinecone index '{self.index_name}'")
# Connect to the index
self.index = self.client.Index(self.index_name)
except Exception as e:
log.error(f"Failed to initialize Pinecone index: {e}")
raise RuntimeError(f"Failed to initialize Pinecone index: {e}")
def _create_points(
self, items: List[VectorItem], collection_name_with_prefix: str
) -> List[Dict[str, Any]]:
"""Convert VectorItem objects to Pinecone point format."""
points = []
for item in items:
# Start with any existing metadata or an empty dict
metadata = item.get("metadata", {}).copy() if item.get("metadata") else {}
# Add text to metadata if available
if "text" in item:
metadata["text"] = item["text"]
# Always add collection_name to metadata for filtering
metadata["collection_name"] = collection_name_with_prefix
point = {
"id": item["id"],
"values": item["vector"],
"metadata": metadata,
}
points.append(point)
return points
def _get_collection_name_with_prefix(self, collection_name: str) -> str:
"""Get the collection name with prefix."""
return f"{self.collection_prefix}_{collection_name}"
def _normalize_distance(self, score: float) -> float:
"""Normalize distance score based on the metric used."""
if self.metric.lower() == "cosine":
# Cosine similarity ranges from -1 to 1, normalize to 0 to 1
return (score + 1.0) / 2.0
elif self.metric.lower() in ["euclidean", "dotproduct"]:
# These are already suitable for ranking (smaller is better for Euclidean)
return score
else:
# For other metrics, use as is
return score
def _result_to_get_result(self, matches: list) -> GetResult:
"""Convert Pinecone matches to GetResult format."""
ids = []
documents = []
metadatas = []
for match in matches:
metadata = match.get("metadata", {})
ids.append(match["id"])
documents.append(metadata.get("text", ""))
metadatas.append(metadata)
return GetResult(
**{
"ids": [ids],
"documents": [documents],
"metadatas": [metadatas],
}
)
def has_collection(self, collection_name: str) -> bool:
"""Check if a collection exists by searching for at least one item."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
# Search for at least 1 item with this collection name in metadata
response = self.index.query(
vector=[0.0] * self.dimension, # dummy vector
top_k=1,
filter={"collection_name": collection_name_with_prefix},
include_metadata=False,
)
return len(response.matches) > 0
except Exception as e:
log.exception(
f"Error checking collection '{collection_name_with_prefix}': {e}"
)
return False
def delete_collection(self, collection_name: str) -> None:
"""Delete a collection by removing all vectors with the collection name in metadata."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
self.index.delete(filter={"collection_name": collection_name_with_prefix})
log.info(
f"Collection '{collection_name_with_prefix}' deleted (all vectors removed)."
)
except Exception as e:
log.warning(
f"Failed to delete collection '{collection_name_with_prefix}': {e}"
)
raise
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert vectors into a collection."""
if not items:
log.warning("No items to insert")
return
start_time = time.time()
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Parallelize batch inserts for performance
executor = self._executor
futures = []
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
futures.append(executor.submit(self.index.upsert, vectors=batch))
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
log.error(f"Error inserting batch: {e}")
raise
elapsed = time.time() - start_time
log.debug(f"Insert of {len(points)} vectors took {elapsed:.2f} seconds")
log.info(
f"Successfully inserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'"
)
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Upsert (insert or update) vectors into a collection."""
if not items:
log.warning("No items to upsert")
return
start_time = time.time()
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Parallelize batch upserts for performance
executor = self._executor
futures = []
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
futures.append(executor.submit(self.index.upsert, vectors=batch))
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
log.error(f"Error upserting batch: {e}")
raise
elapsed = time.time() - start_time
log.debug(f"Upsert of {len(points)} vectors took {elapsed:.2f} seconds")
log.info(
f"Successfully upserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'"
)
async def insert_async(self, collection_name: str, items: List[VectorItem]) -> None:
"""Async version of insert using asyncio and run_in_executor for improved performance."""
if not items:
log.warning("No items to insert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Create batches
batches = [
points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)
]
loop = asyncio.get_event_loop()
tasks = [
loop.run_in_executor(
None, functools.partial(self.index.upsert, vectors=batch)
)
for batch in batches
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
log.error(f"Error in async insert batch: {result}")
raise result
log.info(
f"Successfully async inserted {len(points)} vectors in batches into '{collection_name_with_prefix}'"
)
async def upsert_async(self, collection_name: str, items: List[VectorItem]) -> None:
"""Async version of upsert using asyncio and run_in_executor for improved performance."""
if not items:
log.warning("No items to upsert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Create batches
batches = [
points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)
]
loop = asyncio.get_event_loop()
tasks = [
loop.run_in_executor(
None, functools.partial(self.index.upsert, vectors=batch)
)
for batch in batches
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
log.error(f"Error in async upsert batch: {result}")
raise result
log.info(
f"Successfully async upserted {len(points)} vectors in batches into '{collection_name_with_prefix}'"
)
def streaming_upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Perform a streaming upsert over gRPC for performance testing."""
if not items:
log.warning("No items to upsert via streaming")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Open a streaming upsert channel
stream = self.index.streaming_upsert()
try:
for point in points:
# send each point over the stream
stream.send(point)
# close the stream to finalize
stream.close()
log.info(
f"Successfully streamed upsert of {len(points)} vectors into '{collection_name_with_prefix}'"
)
except Exception as e:
log.error(f"Error during streaming upsert: {e}")
raise
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
if not vectors or not vectors[0]:
log.warning("No vectors provided for search")
return None
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
if limit is None or limit <= 0:
limit = NO_LIMIT
try:
# Search using the first vector (assuming this is the intended behavior)
query_vector = vectors[0]
# Perform the search
query_response = self.index.query(
vector=query_vector,
top_k=limit,
include_metadata=True,
filter={"collection_name": collection_name_with_prefix},
)
if not query_response.matches:
# Return empty result if no matches
return SearchResult(
ids=[[]],
documents=[[]],
metadatas=[[]],
distances=[[]],
)
# Convert to GetResult format
get_result = self._result_to_get_result(query_response.matches)
# Calculate normalized distances based on metric
distances = [
[
self._normalize_distance(match.score)
for match in query_response.matches
]
]
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
distances=distances,
)
except Exception as e:
log.error(f"Error searching in '{collection_name_with_prefix}': {e}")
return None
def query(
self, collection_name: str, filter: Dict, limit: Optional[int] = None
) -> Optional[GetResult]:
"""Query vectors by metadata filter."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
if limit is None or limit <= 0:
limit = NO_LIMIT
try:
# Create a zero vector for the dimension as Pinecone requires a vector
zero_vector = [0.0] * self.dimension
# Combine user filter with collection_name
pinecone_filter = {"collection_name": collection_name_with_prefix}
if filter:
pinecone_filter.update(filter)
# Perform metadata-only query
query_response = self.index.query(
vector=zero_vector,
filter=pinecone_filter,
top_k=limit,
include_metadata=True,
)
return self._result_to_get_result(query_response.matches)
except Exception as e:
log.error(f"Error querying collection '{collection_name}': {e}")
return None
def get(self, collection_name: str) -> Optional[GetResult]:
"""Get all vectors in a collection."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
# Use a zero vector for fetching all entries
zero_vector = [0.0] * self.dimension
# Add filter to only get vectors for this collection
query_response = self.index.query(
vector=zero_vector,
top_k=NO_LIMIT,
include_metadata=True,
filter={"collection_name": collection_name_with_prefix},
)
return self._result_to_get_result(query_response.matches)
except Exception as e:
log.error(f"Error getting collection '{collection_name}': {e}")
return None
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict] = None,
) -> None:
"""Delete vectors by IDs or filter."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
if ids:
# Delete by IDs (in batches for large deletions)
for i in range(0, len(ids), BATCH_SIZE):
batch_ids = ids[i : i + BATCH_SIZE]
# Note: When deleting by ID, we can't filter by collection_name
# This is a limitation of Pinecone - be careful with ID uniqueness
self.index.delete(ids=batch_ids)
log.debug(
f"Deleted batch of {len(batch_ids)} vectors by ID from '{collection_name_with_prefix}'"
)
log.info(
f"Successfully deleted {len(ids)} vectors by ID from '{collection_name_with_prefix}'"
)
elif filter:
# Combine user filter with collection_name
pinecone_filter = {"collection_name": collection_name_with_prefix}
if filter:
pinecone_filter.update(filter)
# Delete by metadata filter
self.index.delete(filter=pinecone_filter)
log.info(
f"Successfully deleted vectors by filter from '{collection_name_with_prefix}'"
)
else:
log.warning("No ids or filter provided for delete operation")
except Exception as e:
log.error(f"Error deleting from collection '{collection_name}': {e}")
raise
def reset(self) -> None:
"""Reset the database by deleting all collections."""
try:
self.index.delete(delete_all=True)
log.info("All vectors successfully deleted from the index.")
except Exception as e:
log.error(f"Failed to reset Pinecone index: {e}")
raise
def close(self):
"""Shut down the gRPC channel and thread pool."""
try:
self.client.close()
log.info("Pinecone gRPC channel closed.")
except Exception as e:
log.warning(f"Failed to close Pinecone gRPC channel: {e}")
self._executor.shutdown(wait=True)
def __enter__(self):
"""Enter context manager."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager, ensuring resources are cleaned up."""
self.close()
@@ -1,12 +1,24 @@
from typing import Optional
import logging
from urllib.parse import urlparse
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
QDRANT_URI,
QDRANT_API_KEY,
QDRANT_ON_DISK,
QDRANT_GRPC_PORT,
QDRANT_PREFER_GRPC,
)
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 999999999
@@ -15,16 +27,34 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient:
class QdrantClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
self.QDRANT_URI = QDRANT_URI
self.QDRANT_API_KEY = QDRANT_API_KEY
self.client = (
Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
if self.QDRANT_URI
else None
)
self.QDRANT_ON_DISK = QDRANT_ON_DISK
self.PREFER_GRPC = QDRANT_PREFER_GRPC
self.GRPC_PORT = QDRANT_GRPC_PORT
if not self.QDRANT_URI:
self.client = None
return
# Unified handling for either scheme
parsed = urlparse(self.QDRANT_URI)
host = parsed.hostname or self.QDRANT_URI
http_port = parsed.port or 6333 # default REST port
if self.PREFER_GRPC:
self.client = Qclient(
host=host,
port=http_port,
grpc_port=self.GRPC_PORT,
prefer_grpc=self.PREFER_GRPC,
api_key=self.QDRANT_API_KEY,
)
else:
self.client = Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
def _result_to_get_result(self, points) -> GetResult:
ids = []
@@ -50,7 +80,9 @@ class QdrantClient:
self.client.create_collection(
collection_name=collection_name_with_prefix,
vectors_config=models.VectorParams(
size=dimension, distance=models.Distance.COSINE
size=dimension,
distance=models.Distance.COSINE,
on_disk=self.QDRANT_ON_DISK,
),
)
@@ -99,7 +131,8 @@ class QdrantClient:
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
distances=[[point.score for point in query_response.points]],
# qdrant distance is [-1, 1], normalize to [0, 1]
distances=[[(point.score + 1.0) / 2.0 for point in query_response.points]],
)
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
+68 -1
View File
@@ -1,5 +1,6 @@
from pydantic import BaseModel
from typing import Optional, List, Any
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Union
class VectorItem(BaseModel):
@@ -17,3 +18,69 @@ class GetResult(BaseModel):
class SearchResult(GetResult):
distances: Optional[List[List[float | int]]]
class VectorDBBase(ABC):
"""
Abstract base class for all vector database backends.
Implementations of this class provide methods for collection management,
vector insertion, deletion, similarity search, and metadata filtering.
Any custom vector database integration must inherit from this class and
implement all abstract methods.
"""
@abstractmethod
def has_collection(self, collection_name: str) -> bool:
"""Check if the collection exists in the vector DB."""
pass
@abstractmethod
def delete_collection(self, collection_name: str) -> None:
"""Delete a collection from the vector DB."""
pass
@abstractmethod
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert a list of vector items into a collection."""
pass
@abstractmethod
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert or update vector items in a collection."""
pass
@abstractmethod
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
pass
@abstractmethod
def query(
self, collection_name: str, filter: Dict, limit: Optional[int] = None
) -> Optional[GetResult]:
"""Query vectors from a collection using metadata filter."""
pass
@abstractmethod
def get(self, collection_name: str) -> Optional[GetResult]:
"""Retrieve all vectors from a collection."""
pass
@abstractmethod
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict] = None,
) -> None:
"""Delete vectors by ID or filter from a collection."""
pass
@abstractmethod
def reset(self) -> None:
"""Reset the vector database by removing all collections or those matching a condition."""
pass
@@ -3,6 +3,7 @@ from typing import Optional
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
from duckduckgo_search.exceptions import RatelimitException
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -22,16 +23,15 @@ def search_duckduckgo(
list[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
search_results = []
with DDGS() as ddgs:
# Use the ddgs.text() method to perform the search
ddgs_gen = ddgs.text(
query, safesearch="moderate", max_results=count, backend="api"
)
# Check if there are search results
if ddgs_gen:
# Convert the search results into a list
search_results = [r for r in ddgs_gen]
try:
search_results = ddgs.text(
query, safesearch="moderate", max_results=count, backend="lite"
)
except RatelimitException as e:
log.error(f"RatelimitException: {e}")
if filter_list:
search_results = get_filtered_results(search_results, filter_list)
@@ -0,0 +1,47 @@
import logging
from typing import Optional, List
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_external(
external_url: str,
external_api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
try:
response = requests.post(
external_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {external_api_key}",
},
json={
"query": query,
"count": count,
},
)
response.raise_for_status()
results = response.json()
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("link"),
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results[:count]
]
log.info(f"External search results: {results}")
return results
except Exception as e:
log.error(f"Error in External search: {e}")
return []
@@ -0,0 +1,49 @@
import logging
from typing import Optional, List
from urllib.parse import urljoin
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_firecrawl(
firecrawl_url: str,
firecrawl_api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
try:
firecrawl_search_url = urljoin(firecrawl_url, "/v1/search")
response = requests.post(
firecrawl_search_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {firecrawl_api_key}",
},
json={
"query": query,
"limit": count,
},
)
response.raise_for_status()
results = response.json().get("data", [])
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]
log.info(f"External search results: {results}")
return results
except Exception as e:
log.error(f"Error in External search: {e}")
return []
@@ -0,0 +1,87 @@
import logging
from typing import Optional, List
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_perplexity(
api_key: str,
query: str,
count: int,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""Search using Perplexity API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Perplexity API key
query (str): The query to search for
count (int): Maximum number of results to return
"""
# Handle PersistentConfig object
if hasattr(api_key, "__str__"):
api_key = str(api_key)
try:
url = "https://api.perplexity.ai/chat/completions"
# Create payload for the API call
payload = {
"model": "sonar",
"messages": [
{
"role": "system",
"content": "You are a search assistant. Provide factual information with citations.",
},
{"role": "user", "content": query},
],
"temperature": 0.2, # Lower temperature for more factual responses
"stream": False,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# Make the API request
response = requests.request("POST", url, json=payload, headers=headers)
# Parse the JSON response
json_response = response.json()
# Extract citations from the response
citations = json_response.get("citations", [])
# Create search results from citations
results = []
for i, citation in enumerate(citations[:count]):
# Extract content from the response to use as snippet
content = ""
if "choices" in json_response and json_response["choices"]:
if i == 0:
content = json_response["choices"][0]["message"]["content"]
result = {"link": citation, "title": f"Source {i+1}", "snippet": content}
results.append(result)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"], title=result["title"], snippet=result["snippet"]
)
for result in results[:count]
]
except Exception as e:
log.error(f"Error searching with Perplexity API: {e}")
return []
@@ -0,0 +1,60 @@
import logging
import json
from typing import Optional, List
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_sougou(
sougou_api_sid: str,
sougou_api_sk: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
from tencentcloud.common.common_client import CommonClient
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
TencentCloudSDKException,
)
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
try:
cred = credential.Credential(sougou_api_sid, sougou_api_sk)
http_profile = HttpProfile()
http_profile.endpoint = "tms.tencentcloudapi.com"
client_profile = ClientProfile()
client_profile.http_profile = http_profile
params = json.dumps({"Query": query, "Cnt": 20})
common_client = CommonClient(
"tms", "2020-12-29", cred, "", profile=client_profile
)
results = [
json.loads(page)
for page in common_client.call_json("SearchPro", json.loads(params))[
"Response"
]["Pages"]
]
sorted_results = sorted(
results, key=lambda x: x.get("scour", 0.0), reverse=True
)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("passage"),
)
for result in sorted_results[:count]
]
except TencentCloudSDKException as err:
log.error(f"Error in Sougou search: {err}")
return []
+12 -5
View File
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.retrieval.web.main import SearchResult
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -21,18 +21,25 @@ def search_tavily(
Args:
api_key (str): A Tavily Search API key
query (str): The query to search for
count (int): The maximum number of results to return
Returns:
list[SearchResult]: A list of search results
"""
url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key}
response = requests.post(url, json=data)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = {"query": query, "max_results": count}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
json_response = response.json()
raw_search_results = json_response.get("results", [])
results = json_response.get("results", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
@@ -40,5 +47,5 @@ def search_tavily(
title=result.get("title", ""),
snippet=result.get("content"),
)
for result in raw_search_results[:count]
for result in results
]
+207 -96
View File
@@ -24,13 +24,20 @@ from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoa
from langchain_community.document_loaders.firecrawl import FireCrawlLoader
from langchain_community.document_loaders.base import BaseLoader
from langchain_core.documents import Document
from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.retrieval.loaders.external import ExternalLoader
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
PLAYWRIGHT_WS_URI,
RAG_WEB_LOADER_ENGINE,
PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
WEB_LOADER_ENGINE,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
TAVILY_API_KEY,
TAVILY_EXTRACT_DEPTH,
EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY,
)
from open_webui.env import SRC_LOG_LEVELS
@@ -113,7 +120,47 @@ def verify_ssl_cert(url: str) -> bool:
return False
class SafeFireCrawlLoader(BaseLoader):
class RateLimitMixin:
async def _wait_for_rate_limit(self):
"""Wait to respect the rate limit if specified."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
await asyncio.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
def _sync_wait_for_rate_limit(self):
"""Synchronous version of rate limit wait."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
time.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
class URLProcessingMixin:
def _verify_ssl_cert(self, url: str) -> bool:
"""Verify SSL certificate for a URL."""
return verify_ssl_cert(url)
async def _safe_process_url(self, url: str) -> bool:
"""Perform safety checks before processing a URL."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
await self._wait_for_rate_limit()
return True
def _safe_process_url_sync(self, url: str) -> bool:
"""Synchronous version of safety checks."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
self._sync_wait_for_rate_limit()
return True
class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
def __init__(
self,
web_paths,
@@ -123,7 +170,7 @@ class SafeFireCrawlLoader(BaseLoader):
continue_on_failure: bool = True,
api_key: Optional[str] = None,
api_url: Optional[str] = None,
mode: Literal["crawl", "scrape", "map"] = "crawl",
mode: Literal["crawl", "scrape", "map"] = "scrape",
proxy: Optional[Dict[str, str]] = None,
params: Optional[Dict] = None,
):
@@ -181,10 +228,13 @@ class SafeFireCrawlLoader(BaseLoader):
mode=self.mode,
params=self.params,
)
yield from loader.lazy_load()
for document in loader.lazy_load():
if not document.metadata.get("source"):
document.metadata["source"] = document.metadata.get("sourceURL")
yield document
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
log.exception(f"Error loading {url}: {e}")
continue
raise e
@@ -201,50 +251,129 @@ class SafeFireCrawlLoader(BaseLoader):
params=self.params,
)
async for document in loader.alazy_load():
if not document.metadata.get("source"):
document.metadata["source"] = document.metadata.get("sourceURL")
yield document
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
log.exception(f"Error loading {url}: {e}")
continue
raise e
def _verify_ssl_cert(self, url: str) -> bool:
return verify_ssl_cert(url)
async def _wait_for_rate_limit(self):
"""Wait to respect the rate limit if specified."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
await asyncio.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
def __init__(
self,
web_paths: Union[str, List[str]],
api_key: str,
extract_depth: Literal["basic", "advanced"] = "basic",
continue_on_failure: bool = True,
requests_per_second: Optional[float] = None,
verify_ssl: bool = True,
trust_env: bool = False,
proxy: Optional[Dict[str, str]] = None,
):
"""Initialize SafeTavilyLoader with rate limiting and SSL verification support.
def _sync_wait_for_rate_limit(self):
"""Synchronous version of rate limit wait."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
time.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
Args:
web_paths: List of URLs/paths to process.
api_key: The Tavily API key.
extract_depth: Depth of extraction ("basic" or "advanced").
continue_on_failure: Whether to continue if extraction of a URL fails.
requests_per_second: Number of requests per second to limit to.
verify_ssl: If True, verify SSL certificates.
trust_env: If True, use proxy settings from environment variables.
proxy: Optional proxy configuration.
"""
# Initialize proxy configuration if using environment variables
proxy_server = proxy.get("server") if proxy else None
if trust_env and not proxy_server:
env_proxies = urllib.request.getproxies()
env_proxy_server = env_proxies.get("https") or env_proxies.get("http")
if env_proxy_server:
if proxy:
proxy["server"] = env_proxy_server
else:
proxy = {"server": env_proxy_server}
async def _safe_process_url(self, url: str) -> bool:
"""Perform safety checks before processing a URL."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
await self._wait_for_rate_limit()
return True
# Store parameters for creating TavilyLoader instances
self.web_paths = web_paths if isinstance(web_paths, list) else [web_paths]
self.api_key = api_key
self.extract_depth = extract_depth
self.continue_on_failure = continue_on_failure
self.verify_ssl = verify_ssl
self.trust_env = trust_env
self.proxy = proxy
def _safe_process_url_sync(self, url: str) -> bool:
"""Synchronous version of safety checks."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
self._sync_wait_for_rate_limit()
return True
# Add rate limiting
self.requests_per_second = requests_per_second
self.last_request_time = None
def lazy_load(self) -> Iterator[Document]:
"""Load documents with rate limiting support, delegating to TavilyLoader."""
valid_urls = []
for url in self.web_paths:
try:
self._safe_process_url_sync(url)
valid_urls.append(url)
except Exception as e:
log.warning(f"SSL verification failed for {url}: {str(e)}")
if not self.continue_on_failure:
raise e
if not valid_urls:
if self.continue_on_failure:
log.warning("No valid URLs to process after SSL verification")
return
raise ValueError("No valid URLs to process after SSL verification")
try:
loader = TavilyLoader(
urls=valid_urls,
api_key=self.api_key,
extract_depth=self.extract_depth,
continue_on_failure=self.continue_on_failure,
)
yield from loader.lazy_load()
except Exception as e:
if self.continue_on_failure:
log.exception(f"Error extracting content from URLs: {e}")
else:
raise e
async def alazy_load(self) -> AsyncIterator[Document]:
"""Async version with rate limiting and SSL verification."""
valid_urls = []
for url in self.web_paths:
try:
await self._safe_process_url(url)
valid_urls.append(url)
except Exception as e:
log.warning(f"SSL verification failed for {url}: {str(e)}")
if not self.continue_on_failure:
raise e
if not valid_urls:
if self.continue_on_failure:
log.warning("No valid URLs to process after SSL verification")
return
raise ValueError("No valid URLs to process after SSL verification")
try:
loader = TavilyLoader(
urls=valid_urls,
api_key=self.api_key,
extract_depth=self.extract_depth,
continue_on_failure=self.continue_on_failure,
)
async for document in loader.alazy_load():
yield document
except Exception as e:
if self.continue_on_failure:
log.exception(f"Error loading URLs: {e}")
else:
raise e
class SafePlaywrightURLLoader(PlaywrightURLLoader):
class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessingMixin):
"""Load HTML pages safely with Playwright, supporting SSL verification, rate limiting, and remote browser connection.
Attributes:
@@ -256,6 +385,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
headless (bool): If True, the browser will run in headless mode.
proxy (dict): Proxy override settings for the Playwright session.
playwright_ws_url (Optional[str]): WebSocket endpoint URI for remote browser connection.
playwright_timeout (Optional[int]): Maximum operation time in milliseconds.
"""
def __init__(
@@ -269,6 +399,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
remove_selectors: Optional[List[str]] = None,
proxy: Optional[Dict[str, str]] = None,
playwright_ws_url: Optional[str] = None,
playwright_timeout: Optional[int] = 10000,
):
"""Initialize with additional safety parameters and remote browser support."""
@@ -295,6 +426,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
self.last_request_time = None
self.playwright_ws_url = playwright_ws_url
self.trust_env = trust_env
self.playwright_timeout = playwright_timeout
def lazy_load(self) -> Iterator[Document]:
"""Safely load URLs synchronously with support for remote browser."""
@@ -311,7 +443,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
try:
self._safe_process_url_sync(url)
page = browser.new_page()
response = page.goto(url)
response = page.goto(url, timeout=self.playwright_timeout)
if response is None:
raise ValueError(f"page.goto() returned None for url {url}")
@@ -320,7 +452,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
yield Document(page_content=text, metadata=metadata)
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
log.exception(f"Error loading {url}: {e}")
continue
raise e
browser.close()
@@ -342,7 +474,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
try:
await self._safe_process_url(url)
page = await browser.new_page()
response = await page.goto(url)
response = await page.goto(url, timeout=self.playwright_timeout)
if response is None:
raise ValueError(f"page.goto() returned None for url {url}")
@@ -351,46 +483,11 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader):
yield Document(page_content=text, metadata=metadata)
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
log.exception(f"Error loading {url}: {e}")
continue
raise e
await browser.close()
def _verify_ssl_cert(self, url: str) -> bool:
return verify_ssl_cert(url)
async def _wait_for_rate_limit(self):
"""Wait to respect the rate limit if specified."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
await asyncio.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
def _sync_wait_for_rate_limit(self):
"""Synchronous version of rate limit wait."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
time.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
async def _safe_process_url(self, url: str) -> bool:
"""Perform safety checks before processing a URL."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
await self._wait_for_rate_limit()
return True
def _safe_process_url_sync(self, url: str) -> bool:
"""Synchronous version of safety checks."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
self._sync_wait_for_rate_limit()
return True
class SafeWebBaseLoader(WebBaseLoader):
"""WebBaseLoader with enhanced error handling for URLs."""
@@ -472,7 +569,7 @@ class SafeWebBaseLoader(WebBaseLoader):
yield Document(page_content=text, metadata=metadata)
except Exception as e:
# Log the error and continue with the next URL
log.exception(e, "Error loading %s", path)
log.exception(f"Error loading {path}: {e}")
async def alazy_load(self) -> AsyncIterator[Document]:
"""Async lazy load text from the url(s) in web_path."""
@@ -495,12 +592,6 @@ class SafeWebBaseLoader(WebBaseLoader):
return [document async for document in self.alazy_load()]
RAG_WEB_LOADER_ENGINES = defaultdict(lambda: SafeWebBaseLoader)
RAG_WEB_LOADER_ENGINES["playwright"] = SafePlaywrightURLLoader
RAG_WEB_LOADER_ENGINES["safe_web"] = SafeWebBaseLoader
RAG_WEB_LOADER_ENGINES["firecrawl"] = SafeFireCrawlLoader
def get_web_loader(
urls: Union[str, Sequence[str]],
verify_ssl: bool = True,
@@ -518,21 +609,41 @@ def get_web_loader(
"trust_env": trust_env,
}
if PLAYWRIGHT_WS_URI.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URI.value
if WEB_LOADER_ENGINE.value == "" or WEB_LOADER_ENGINE.value == "safe_web":
WebLoaderClass = SafeWebBaseLoader
if WEB_LOADER_ENGINE.value == "playwright":
WebLoaderClass = SafePlaywrightURLLoader
web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value * 1000
if PLAYWRIGHT_WS_URL.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value
if RAG_WEB_LOADER_ENGINE.value == "firecrawl":
if WEB_LOADER_ENGINE.value == "firecrawl":
WebLoaderClass = SafeFireCrawlLoader
web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
# Create the appropriate WebLoader based on the configuration
WebLoaderClass = RAG_WEB_LOADER_ENGINES[RAG_WEB_LOADER_ENGINE.value]
web_loader = WebLoaderClass(**web_loader_args)
if WEB_LOADER_ENGINE.value == "tavily":
WebLoaderClass = SafeTavilyLoader
web_loader_args["api_key"] = TAVILY_API_KEY.value
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
log.debug(
"Using RAG_WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
)
if WEB_LOADER_ENGINE.value == "external":
WebLoaderClass = ExternalLoader
web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value
web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value
return web_loader
if WebLoaderClass:
web_loader = WebLoaderClass(**web_loader_args)
log.debug(
"Using WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
)
return web_loader
else:
raise ValueError(
f"Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. "
"Please set it to 'safe_web', 'playwright', 'firecrawl', or 'tavily'."
)
+87
View File
@@ -0,0 +1,87 @@
import logging
from typing import Optional
import requests
from requests.auth import HTTPDigestAuth
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_yacy(
query_url: str,
username: Optional[str],
password: Optional[str],
query: str,
count: int,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""
Search a Yacy instance for a given query and return the results as a list of SearchResult objects.
The function accepts username and password for authenticating to Yacy.
Args:
query_url (str): The base URL of the Yacy server.
username (str): Optional YaCy username.
password (str): Optional YaCy password.
query (str): The search term or question to find in the Yacy database.
count (int): The maximum number of results to retrieve from the search.
Returns:
list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
Raise:
requests.exceptions.RequestException: If a request error occurs during the search process.
"""
# Use authentication if either username or password is set
yacy_auth = None
if username or password:
yacy_auth = HTTPDigestAuth(username, password)
params = {
"query": query,
"contentdom": "text",
"resource": "global",
"maximumRecords": count,
"nav": "none",
}
# Check if provided a json API URL
if not query_url.endswith("yacysearch.json"):
# Strip all query parameters from the URL
query_url = query_url.rstrip("/") + "/yacysearch.json"
log.debug(f"searching {query_url}")
response = requests.get(
query_url,
auth=yacy_auth,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "keep-alive",
},
params=params,
)
response.raise_for_status() # Raise an exception for HTTP errors.
json_response = response.json()
results = json_response.get("channels", [{}])[0].get("items", [])
sorted_results = sorted(results, key=lambda x: x.get("ranking", 0), reverse=True)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in sorted_results[:count]
]
+209 -26
View File
@@ -33,6 +33,7 @@ from open_webui.config import (
WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR,
CACHE_DIR,
WHISPER_LANGUAGE,
)
from open_webui.constants import ERROR_MESSAGES
@@ -50,11 +51,13 @@ router = APIRouter()
# Constants
MAX_FILE_SIZE_MB = 25
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
AZURE_MAX_FILE_SIZE_MB = 200
AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
@@ -68,25 +71,33 @@ from pydub import AudioSegment
from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
def get_audio_convert_format(file_path):
"""Check if the given file needs to be converted to a different format."""
if not os.path.isfile(file_path):
log.error(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return True
return False
try:
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return "mp4"
elif info.get("format_name") == "ogg":
return "ogg"
except Exception as e:
log.error(f"Error getting audio format: {e}")
return False
return None
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
def convert_audio_to_wav(file_path, output_path, conversion_type):
"""Convert MP4/OGG audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format=conversion_type)
audio.export(output_path, format="wav")
log.info(f"Converted {file_path} to {output_path}")
@@ -131,6 +142,7 @@ class TTSConfigForm(BaseModel):
VOICE: str
SPLIT_ON: str
AZURE_SPEECH_REGION: str
AZURE_SPEECH_BASE_URL: str
AZURE_SPEECH_OUTPUT_FORMAT: str
@@ -141,6 +153,11 @@ class STTConfigForm(BaseModel):
MODEL: str
WHISPER_MODEL: str
DEEPGRAM_API_KEY: str
AZURE_API_KEY: str
AZURE_REGION: str
AZURE_LOCALES: str
AZURE_BASE_URL: str
AZURE_MAX_SPEAKERS: str
class AudioConfigUpdateForm(BaseModel):
@@ -160,6 +177,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"VOICE": request.app.state.config.TTS_VOICE,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
"AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
@@ -169,6 +187,11 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"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,
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
},
}
@@ -185,6 +208,9 @@ async def update_audio_config(
request.app.state.config.TTS_VOICE = form_data.tts.VOICE
request.app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
request.app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
request.app.state.config.TTS_AZURE_SPEECH_BASE_URL = (
form_data.tts.AZURE_SPEECH_BASE_URL
)
request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
)
@@ -195,6 +221,13 @@ async def update_audio_config(
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
request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY
request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION
request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES
request.app.state.config.AUDIO_STT_AZURE_BASE_URL = form_data.stt.AZURE_BASE_URL
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = (
form_data.stt.AZURE_MAX_SPEAKERS
)
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
@@ -211,6 +244,7 @@ async def update_audio_config(
"VOICE": request.app.state.config.TTS_VOICE,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
"AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
@@ -220,6 +254,11 @@ async def update_audio_config(
"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,
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
},
}
@@ -312,7 +351,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -366,7 +405,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -377,7 +416,8 @@ async def speech(request: Request, user=Depends(get_verified_user)):
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
region = request.app.state.config.TTS_AZURE_SPEECH_REGION or "eastus"
base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL
language = request.app.state.config.TTS_VOICE
locale = "-".join(request.app.state.config.TTS_VOICE.split("-")[:1])
output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
@@ -391,7 +431,8 @@ async def speech(request: Request, user=Depends(get_verified_user)):
timeout=timeout, trust_env=True
) as session:
async with session.post(
f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1",
(base_url or f"https://{region}.tts.speech.microsoft.com")
+ "/cognitiveservices/v1",
headers={
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY,
"Content-Type": "application/ssml+xml",
@@ -422,7 +463,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -479,7 +520,12 @@ def transcribe(request: Request, file_path):
)
model = request.app.state.faster_whisper_model
segments, info = model.transcribe(file_path, beam_size=5)
segments, info = model.transcribe(
file_path,
beam_size=5,
vad_filter=request.app.state.config.WHISPER_VAD_FILTER,
language=WHISPER_LANGUAGE,
)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
@@ -496,10 +542,18 @@ def transcribe(request: Request, file_path):
log.debug(data)
return data
elif request.app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
os.rename(file_path, file_path.replace(".wav", ".mp4"))
# Convert MP4 audio file to WAV format
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
convert_format = get_audio_convert_format(file_path)
if convert_format:
ext = convert_format.split(".")[-1]
os.rename(file_path, file_path.replace(".{ext}", f".{convert_format}"))
# Convert unsupported audio file to WAV format
convert_audio_to_wav(
file_path.replace(".{ext}", f".{convert_format}"),
file_path,
convert_format,
)
r = None
try:
@@ -598,6 +652,124 @@ def transcribe(request: Request, file_path):
detail = f"External: {e}"
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
elif request.app.state.config.STT_ENGINE == "azure":
# Check file exists and size
if not os.path.exists(file_path):
raise HTTPException(status_code=400, detail="Audio file not found")
# Check file size (Azure has a larger limit of 200MB)
file_size = os.path.getsize(file_path)
if file_size > AZURE_MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File size exceeds Azure's limit of {AZURE_MAX_FILE_SIZE_MB}MB",
)
api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
region = request.app.state.config.AUDIO_STT_AZURE_REGION or "eastus"
locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES
base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL
max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS or 3
# IF NO LOCALES, USE DEFAULTS
if len(locales) < 2:
locales = [
"en-US",
"es-ES",
"es-MX",
"fr-FR",
"hi-IN",
"it-IT",
"de-DE",
"en-GB",
"en-IN",
"ja-JP",
"ko-KR",
"pt-BR",
"zh-CN",
]
locales = ",".join(locales)
if not api_key or not region:
raise HTTPException(
status_code=400,
detail="Azure API key is required for Azure STT",
)
r = None
try:
# Prepare the request
data = {
"definition": json.dumps(
{
"locales": locales.split(","),
"diarization": {"maxSpeakers": max_speakers, "enabled": True},
}
if locales
else {}
)
}
url = (
base_url or f"https://{region}.api.cognitive.microsoft.com"
) + "/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
# Use context manager to ensure file is properly closed
with open(file_path, "rb") as audio_file:
r = requests.post(
url=url,
files={"audio": audio_file},
data=data,
headers={
"Ocp-Apim-Subscription-Key": api_key,
},
)
r.raise_for_status()
response = r.json()
# Extract transcript from response
if not response.get("combinedPhrases"):
raise ValueError("No transcription found in response")
# Get the full transcript from combinedPhrases
transcript = response["combinedPhrases"][0].get("text", "").strip()
if not transcript:
raise ValueError("Empty transcript in response")
data = {"text": transcript}
# Save transcript to json file (consistent with other providers)
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
log.debug(data)
return data
except (KeyError, IndexError, ValueError) as e:
log.exception("Error parsing Azure response")
raise HTTPException(
status_code=500,
detail=f"Failed to parse Azure response: {str(e)}",
)
except requests.exceptions.RequestException as e:
log.exception(e)
detail = None
try:
if r is not None and r.status_code != 200:
res = r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status_code", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE:
@@ -625,7 +797,15 @@ def transcription(
):
log.info(f"file.content_type: {file.content_type}")
if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
supported_filetypes = (
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
"audio/webm",
)
if not file.content_type.startswith(supported_filetypes):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
@@ -767,7 +947,10 @@ def get_available_voices(request) -> dict:
elif request.app.state.config.TTS_ENGINE == "azure":
try:
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL
url = (
base_url or f"https://{region}.tts.speech.microsoft.com"
) + "/cognitiveservices/voices/list"
headers = {
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY
}
+112 -52
View File
@@ -27,20 +27,24 @@ from open_webui.env import (
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
SRC_LOG_LEVELS,
)
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import (
decode_token,
create_api_key,
create_token,
get_admin_user,
get_verified_user,
get_current_user,
get_password_hash,
get_http_authorization_cred,
)
from open_webui.utils.webhook import post_webhook
from open_webui.utils.access_control import get_permissions
@@ -72,31 +76,36 @@ class SessionUserResponse(Token, UserResponse):
async def get_session_user(
request: Request, response: Response, user=Depends(get_current_user)
):
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
auth_header = request.headers.get("Authorization")
auth_token = get_http_authorization_cred(auth_header)
token = auth_token.credentials
data = decode_token(token)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token(
data={"id": user.id},
expires_delta=expires_delta,
)
if data:
expires_at = data.get("exp")
datetime_expires_at = (
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
)
if (expires_at is not None) and int(time.time()) > expires_at:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=datetime_expires_at,
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=(
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
),
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
@@ -194,8 +203,8 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
ciphers=LDAP_CIPHERS,
)
except Exception as e:
log.error(f"An error occurred on TLS: {str(e)}")
raise HTTPException(400, detail=str(e))
log.error(f"TLS configuration error: {str(e)}")
raise HTTPException(400, detail="Failed to configure TLS for LDAP connection.")
try:
server = Server(
@@ -210,7 +219,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
LDAP_APP_DN,
LDAP_APP_PASSWORD,
auto_bind="NONE",
authentication="SIMPLE",
authentication="SIMPLE" if LDAP_APP_DN else "ANONYMOUS",
)
if not connection_app.bind():
raise HTTPException(400, detail="Application account bind failed")
@@ -225,14 +234,23 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
],
)
if not search_success:
if not search_success or not connection_app.entries:
raise HTTPException(400, detail="User not found in the LDAP server")
entry = connection_app.entries[0]
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
mail = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
if not mail or mail == "" or mail == "[]":
raise HTTPException(400, f"User {form_data.user} does not have mail.")
email = entry[
f"{LDAP_ATTRIBUTE_FOR_MAIL}"
].value # retrieve the Attribute value
if not email:
raise HTTPException(400, "User does not have a valid email address.")
elif isinstance(email, str):
email = email.lower()
elif isinstance(email, list):
email = email[0].lower()
else:
email = str(email).lower()
cn = str(entry["cn"])
user_dn = entry.entry_dn
@@ -245,9 +263,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
authentication="SIMPLE",
)
if not connection_user.bind():
raise HTTPException(400, f"Authentication failed for {form_data.user}")
raise HTTPException(400, "Authentication failed.")
user = Users.get_user_by_email(mail)
user = Users.get_user_by_email(email)
if not user:
try:
user_count = Users.get_num_users()
@@ -259,7 +277,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
)
user = Auths.insert_new_auth(
email=mail, password=str(uuid.uuid4()), name=cn, role=role
email=email,
password=str(uuid.uuid4()),
name=cn,
role=role,
)
if not user:
@@ -270,23 +291,38 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
except HTTPException:
raise
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
log.error(f"LDAP user creation error: {str(err)}")
raise HTTPException(
500, detail="Internal error occurred during LDAP user creation."
)
user = Auths.authenticate_user_by_trusted_header(mail)
user = Auths.authenticate_user_by_trusted_header(email)
if user:
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(
request.app.state.config.JWT_EXPIRES_IN
),
expires_delta=expires_delta,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=(
datetime.datetime.fromtimestamp(
expires_at, datetime.timezone.utc
)
if expires_at
else None
),
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
user_permissions = get_permissions(
@@ -296,6 +332,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
return {
"token": token,
"token_type": "Bearer",
"expires_at": expires_at,
"id": user.id,
"email": user.email,
"name": user.name,
@@ -306,12 +343,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
else:
raise HTTPException(
400,
f"User {form_data.user} does not match the record. Search result: {str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'])}",
)
raise HTTPException(400, "User record mismatch.")
except Exception as e:
raise HTTPException(400, detail=str(e))
log.error(f"LDAP authentication error: {str(e)}")
raise HTTPException(400, detail="LDAP authentication failed.")
############################
@@ -447,6 +482,13 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(form_data.password.encode("utf-8")) > 72:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.PASSWORD_TOO_LONG,
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
@@ -513,7 +555,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
log.error(f"Signup error: {str(err)}")
raise HTTPException(500, detail="An internal error occurred during signup.")
@router.get("/signout")
@@ -541,7 +584,17 @@ async def signout(request: Request, response: Response):
detail="Failed to fetch OpenID configuration",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
log.error(f"OpenID signout error: {str(e)}")
raise HTTPException(
status_code=500,
detail="Failed to sign out from the OpenID provider.",
)
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
return RedirectResponse(
headers=response.headers,
url=WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
)
return {"status": True}
@@ -585,7 +638,10 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
log.error(f"Add user error: {str(err)}")
raise HTTPException(
500, detail="An internal error occurred while adding the user."
)
############################
@@ -633,11 +689,13 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
"API_KEY_ALLOWED_ENDPOINTS": request.app.state.config.API_KEY_ALLOWED_ENDPOINTS,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
}
@@ -648,11 +706,13 @@ class AdminConfig(BaseModel):
ENABLE_API_KEY: bool
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS: bool
API_KEY_ALLOWED_ENDPOINTS: str
ENABLE_CHANNELS: bool
DEFAULT_USER_ROLE: str
JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool
ENABLE_MESSAGE_RATING: bool
ENABLE_CHANNELS: bool
ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool
@router.post("/admin/config")
@@ -672,6 +732,7 @@ async def update_admin_config(
)
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
@@ -687,6 +748,8 @@ async def update_admin_config(
)
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"WEBUI_URL": request.app.state.config.WEBUI_URL,
@@ -694,11 +757,13 @@ async def update_admin_config(
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
"API_KEY_ALLOWED_ENDPOINTS": request.app.state.config.API_KEY_ALLOWED_ENDPOINTS,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
}
@@ -753,11 +818,6 @@ async def update_ldap_server(
if not value:
raise HTTPException(400, detail=f"Required field {key} is empty")
if form_data.use_tls and not form_data.certificate_path:
raise HTTPException(
400, detail="TLS is enabled but certificate file path is missing"
)
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
request.app.state.config.LDAP_SERVER_HOST = form_data.host
request.app.state.config.LDAP_SERVER_PORT = form_data.port
+119 -2
View File
@@ -2,6 +2,8 @@ import json
import logging
from typing import Optional
from open_webui.socket.main import get_event_emitter
from open_webui.models.chats import (
ChatForm,
ChatImportForm,
@@ -372,6 +374,107 @@ async def update_chat_by_id(
)
############################
# UpdateChatMessageById
############################
class MessageForm(BaseModel):
content: str
@router.post("/{id}/messages/{message_id}", response_model=Optional[ChatResponse])
async def update_chat_message_by_id(
id: str, message_id: str, form_data: MessageForm, user=Depends(get_verified_user)
):
chat = Chats.get_chat_by_id(id)
if not chat:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if chat.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
chat = Chats.upsert_message_to_chat_by_id_and_message_id(
id,
message_id,
{
"content": form_data.content,
},
)
event_emitter = get_event_emitter(
{
"user_id": user.id,
"chat_id": id,
"message_id": message_id,
},
False,
)
if event_emitter:
await event_emitter(
{
"type": "chat:message",
"data": {
"chat_id": id,
"message_id": message_id,
"content": form_data.content,
},
}
)
return ChatResponse(**chat.model_dump())
############################
# SendChatMessageEventById
############################
class EventForm(BaseModel):
type: str
data: dict
@router.post("/{id}/messages/{message_id}/event", response_model=Optional[bool])
async def send_chat_message_event_by_id(
id: str, message_id: str, form_data: EventForm, user=Depends(get_verified_user)
):
chat = Chats.get_chat_by_id(id)
if not chat:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if chat.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
event_emitter = get_event_emitter(
{
"user_id": user.id,
"chat_id": id,
"message_id": message_id,
}
)
try:
if event_emitter:
await event_emitter(form_data.model_dump())
else:
return False
return True
except:
return False
############################
# DeleteChatById
############################
@@ -476,7 +579,12 @@ async def clone_chat_by_id(
@router.post("/{id}/clone/shared", response_model=Optional[ChatResponse])
async def clone_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_share_id(id)
if user.role == "admin":
chat = Chats.get_chat_by_id(id)
else:
chat = Chats.get_chat_by_share_id(id)
if chat:
updated_chat = {
**chat.chat,
@@ -530,8 +638,17 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if not has_permission(
user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
+78 -2
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from fastapi import APIRouter, Depends, Request, HTTPException
from pydantic import BaseModel, ConfigDict
from typing import Optional
@@ -7,6 +7,8 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.config import get_config, save_config
from open_webui.config import BannerModel
from open_webui.utils.tools import get_tool_server_data, get_tool_servers_data
router = APIRouter()
@@ -66,10 +68,80 @@ async def set_direct_connections_config(
}
############################
# ToolServers Config
############################
class ToolServerConnection(BaseModel):
url: str
path: str
auth_type: Optional[str]
key: Optional[str]
config: Optional[dict]
model_config = ConfigDict(extra="allow")
class ToolServersConfigForm(BaseModel):
TOOL_SERVER_CONNECTIONS: list[ToolServerConnection]
@router.get("/tool_servers", response_model=ToolServersConfigForm)
async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)):
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers", response_model=ToolServersConfigForm)
async def set_tool_servers_config(
request: Request,
form_data: ToolServersConfigForm,
user=Depends(get_admin_user),
):
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
]
request.app.state.TOOL_SERVERS = await get_tool_servers_data(
request.app.state.config.TOOL_SERVER_CONNECTIONS
)
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers/verify")
async def verify_tool_servers_config(
request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)
):
"""
Verify the connection to the tool server.
"""
try:
token = None
if form_data.auth_type == "bearer":
token = form_data.key
elif form_data.auth_type == "session":
token = request.state.token.credentials
url = f"{form_data.url}/{form_data.path}"
return await get_tool_server_data(token, url)
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to connect to the tool server: {str(e)}",
)
############################
# CodeInterpreterConfig
############################
class CodeInterpreterConfigForm(BaseModel):
ENABLE_CODE_EXECUTION: bool
CODE_EXECUTION_ENGINE: str
CODE_EXECUTION_JUPYTER_URL: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
@@ -89,6 +161,7 @@ class CodeInterpreterConfigForm(BaseModel):
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
@@ -111,6 +184,8 @@ async def set_code_execution_config(
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
form_data.CODE_EXECUTION_JUPYTER_URL
@@ -153,6 +228,7 @@ async def set_code_execution_config(
)
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
+14 -2
View File
@@ -56,8 +56,19 @@ async def update_config(
}
class UserResponse(BaseModel):
id: str
name: str
email: str
role: str = "pending"
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class FeedbackUserResponse(FeedbackResponse):
user: Optional[UserModel] = None
user: Optional[UserResponse] = None
@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
@@ -65,7 +76,8 @@ async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks()
return [
FeedbackUserResponse(
**feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
**feedback.model_dump(),
user=UserResponse(**Users.get_user_by_id(feedback.user_id).model_dump()),
)
for feedback in feedbacks
]
+251 -46
View File
@@ -1,20 +1,35 @@
import logging
import os
import uuid
from fnmatch import fnmatch
from pathlib import Path
from typing import Optional
from urllib.parse import quote
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
from fastapi import (
APIRouter,
Depends,
File,
HTTPException,
Request,
UploadFile,
status,
Query,
)
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.models.users import Users
from open_webui.models.files import (
FileForm,
FileModel,
FileModelResponse,
Files,
)
from open_webui.models.knowledge import Knowledges
from open_webui.routers.knowledge import get_knowledge, get_knowledge_list
from open_webui.routers.retrieval import ProcessFileForm, process_file
from open_webui.routers.audio import transcribe
from open_webui.storage.provider import Storage
@@ -27,6 +42,39 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# Check if the current user has access to a file through any knowledge bases the user may be in.
############################
def has_access_to_file(
file_id: Optional[str], access_type: str, user=Depends(get_verified_user)
) -> bool:
file = Files.get_file_by_id(file_id)
log.debug(f"Checking if user has {access_type} access to file")
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
has_access = False
knowledge_base_id = file.meta.get("collection_name") if file.meta else None
if knowledge_base_id:
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(
user.id, access_type
)
for knowledge_base in knowledge_bases:
if knowledge_base.id == knowledge_base_id:
has_access = True
break
return has_access
############################
# Upload File
############################
@@ -37,9 +85,12 @@ def upload_file(
request: Request,
file: UploadFile = File(...),
user=Depends(get_verified_user),
file_metadata: dict = {},
file_metadata: dict = None,
process: bool = Query(True),
):
log.info(f"file.content_type: {file.content_type}")
file_metadata = file_metadata if file_metadata else {}
try:
unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
@@ -48,7 +99,13 @@ def upload_file(
id = str(uuid.uuid4())
name = filename
filename = f"{id}_{filename}"
contents, file_path = Storage.upload_file(file.file, filename)
tags = {
"OpenWebUI-User-Email": user.email,
"OpenWebUI-User-Id": user.id,
"OpenWebUI-User-Name": user.name,
"OpenWebUI-File-Id": id,
}
contents, file_path = Storage.upload_file(file.file, filename, tags)
file_item = Files.insert_new_file(
user.id,
@@ -66,34 +123,47 @@ def upload_file(
}
),
)
if process:
try:
try:
if file.content_type in [
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
]:
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
if file.content_type.startswith(
(
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
"audio/webm",
"video/webm",
)
):
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
elif file.content_type not in [
"image/png",
"image/jpeg",
"image/gif",
"video/mp4",
"video/ogg",
"video/quicktime",
]:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
log.error(f"Error processing file: {file_item.id}")
file_item = FileModelResponse(
**{
**file_item.model_dump(),
"error": str(e.detail) if hasattr(e, "detail") else str(e),
}
)
else:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
log.error(f"Error processing file: {file_item.id}")
file_item = FileModelResponse(
**{
**file_item.model_dump(),
"error": str(e.detail) if hasattr(e, "detail") else str(e),
}
)
if file_item:
return file_item
@@ -117,14 +187,62 @@ def upload_file(
@router.get("/", response_model=list[FileModelResponse])
async def list_files(user=Depends(get_verified_user)):
async def list_files(user=Depends(get_verified_user), content: bool = Query(True)):
if user.role == "admin":
files = Files.get_files()
else:
files = Files.get_files_by_user_id(user.id)
if not content:
for file in files:
if "content" in file.data:
del file.data["content"]
return files
############################
# Search Files
############################
@router.get("/search", response_model=list[FileModelResponse])
async def search_files(
filename: str = Query(
...,
description="Filename pattern to search for. Supports wildcards such as '*.txt'",
),
content: bool = Query(True),
user=Depends(get_verified_user),
):
"""
Search for files by filename with support for wildcard patterns.
"""
# Get files according to user role
if user.role == "admin":
files = Files.get_files()
else:
files = Files.get_files_by_user_id(user.id)
# Get matching files
matching_files = [
file for file in files if fnmatch(file.filename.lower(), filename.lower())
]
if not matching_files:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No files found matching the pattern.",
)
if not content:
for file in matching_files:
if "content" in file.data:
del file.data["content"]
return matching_files
############################
# Delete All Files
############################
@@ -160,7 +278,17 @@ async def delete_all_files(user=Depends(get_admin_user)):
async def get_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user)
):
return file
else:
raise HTTPException(
@@ -178,7 +306,17 @@ async def get_file_by_id(id: str, user=Depends(get_verified_user)):
async def get_file_data_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user)
):
return {"content": file.data.get("content", "")}
else:
raise HTTPException(
@@ -202,7 +340,17 @@ async def update_file_data_content_by_id(
):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "write", user)
):
try:
process_file(
request,
@@ -228,9 +376,22 @@ async def update_file_data_content_by_id(
@router.get("/{id}/content")
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
async def get_file_content_by_id(
id: str, user=Depends(get_verified_user), attachment: bool = Query(False)
):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user)
):
try:
file_path = Storage.get_file(file.path)
file_path = Path(file_path)
@@ -246,17 +407,22 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
encoded_filename = quote(filename)
headers = {}
if content_type == "application/pdf" or filename.lower().endswith(
".pdf"
):
headers["Content-Disposition"] = (
f"inline; filename*=UTF-8''{encoded_filename}"
)
content_type = "application/pdf"
elif content_type != "text/plain":
if attachment:
headers["Content-Disposition"] = (
f"attachment; filename*=UTF-8''{encoded_filename}"
)
else:
if content_type == "application/pdf" or filename.lower().endswith(
".pdf"
):
headers["Content-Disposition"] = (
f"inline; filename*=UTF-8''{encoded_filename}"
)
content_type = "application/pdf"
elif content_type != "text/plain":
headers["Content-Disposition"] = (
f"attachment; filename*=UTF-8''{encoded_filename}"
)
return FileResponse(file_path, headers=headers, media_type=content_type)
@@ -282,7 +448,25 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
@router.get("/{id}/content/html")
async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
file_user = Users.get_user_by_id(file.user_id)
if not file_user.role == "admin":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user)
):
try:
file_path = Storage.get_file(file.path)
file_path = Path(file_path)
@@ -314,7 +498,17 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user)
):
file_path = file.path
# Handle Unicode filenames
@@ -365,7 +559,18 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
@router.delete("/{id}")
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "write", user)
):
# We should add Chroma cleanup here
result = Files.delete_file_by_id(id)
+16 -2
View File
@@ -20,11 +20,13 @@ from open_webui.env import SRC_LOG_LEVELS
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Request
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_permission
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -228,7 +230,19 @@ async def update_folder_is_expanded_by_id(
@router.delete("/{id}")
async def delete_folder_by_id(id: str, user=Depends(get_verified_user)):
async def delete_folder_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
chat_delete_permission = has_permission(
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
)
if user.role != "admin" and not chat_delete_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
try:
+1 -1
View File
@@ -74,7 +74,7 @@ async def create_new_function(
function = Functions.insert_new_function(user.id, function_type, form_data)
function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
function_cache_dir = CACHE_DIR / "functions" / form_data.id
function_cache_dir.mkdir(parents=True, exist_ok=True)
if function:
+12 -4
View File
@@ -25,7 +25,7 @@ from pydantic import BaseModel
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
IMAGE_CACHE_DIR = CACHE_DIR / "image" / "generations"
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
@@ -500,7 +500,11 @@ async def image_generations(
if form_data.size
else request.app.state.config.IMAGE_SIZE
),
"response_format": "b64_json",
**(
{}
if "gpt-image-1" in request.app.state.config.IMAGE_GENERATION_MODEL
else {"response_format": "b64_json"}
),
}
# Use asyncio.to_thread for the requests.post call
@@ -517,7 +521,11 @@ async def image_generations(
images = []
for image in res["data"]:
image_data, content_type = load_b64_image_data(image["b64_json"])
if image_url := image.get("url", None):
image_data, content_type = load_url_image_data(image_url, headers)
else:
image_data, content_type = load_b64_image_data(image["b64_json"])
url = upload_image(request, data, image_data, content_type, user)
images.append({"url": url})
return images
@@ -615,7 +623,7 @@ async def image_generations(
or request.app.state.config.IMAGE_GENERATION_ENGINE == ""
):
if form_data.model:
set_image_model(form_data.model)
set_image_model(request, form_data.model)
data = {
"prompt": form_data.prompt,
+107 -15
View File
@@ -9,7 +9,7 @@ from open_webui.models.knowledge import (
KnowledgeResponse,
KnowledgeUserResponse,
)
from open_webui.models.files import Files, FileModel
from open_webui.models.files import Files, FileModel, FileMetadataResponse
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.routers.retrieval import (
process_file,
@@ -161,13 +161,94 @@ async def create_new_knowledge(
)
############################
# ReindexKnowledgeFiles
############################
@router.post("/reindex", response_model=bool)
async def reindex_knowledge_files(request: Request, user=Depends(get_verified_user)):
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
knowledge_bases = Knowledges.get_knowledge_bases()
log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
deleted_knowledge_bases = []
for knowledge_base in knowledge_bases:
# -- Robust error handling for missing or invalid data
if not knowledge_base.data or not isinstance(knowledge_base.data, dict):
log.warning(
f"Knowledge base {knowledge_base.id} has no data or invalid data ({knowledge_base.data!r}). Deleting."
)
try:
Knowledges.delete_knowledge_by_id(id=knowledge_base.id)
deleted_knowledge_bases.append(knowledge_base.id)
except Exception as e:
log.error(
f"Failed to delete invalid knowledge base {knowledge_base.id}: {e}"
)
continue
try:
file_ids = knowledge_base.data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
try:
if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
VECTOR_DB_CLIENT.delete_collection(
collection_name=knowledge_base.id
)
except Exception as e:
log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}")
continue # Skip, don't raise
failed_files = []
for file in files:
try:
process_file(
request,
ProcessFileForm(
file_id=file.id, collection_name=knowledge_base.id
),
user=user,
)
except Exception as e:
log.error(
f"Error processing file {file.filename} (ID: {file.id}): {str(e)}"
)
failed_files.append({"file_id": file.id, "error": str(e)})
continue
except Exception as e:
log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}")
# Don't raise, just continue
continue
if failed_files:
log.warning(
f"Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}"
)
for failed in failed_files:
log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}")
log.info(
f"Reindexing completed. Deleted {len(deleted_knowledge_bases)} invalid knowledge bases: {deleted_knowledge_bases}"
)
return True
############################
# GetKnowledgeById
############################
class KnowledgeFilesResponse(KnowledgeResponse):
files: list[FileModel]
files: list[FileMetadataResponse]
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
@@ -183,7 +264,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
):
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -311,7 +392,7 @@ def add_file_to_knowledge_by_id(
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -388,7 +469,7 @@ def update_file_from_knowledge_by_id(
data = knowledge.data or {}
file_ids = data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -437,14 +518,24 @@ def remove_file_from_knowledge_by_id(
)
# Remove content from the vector database
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
)
try:
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
)
except Exception as e:
log.debug("This was most likely caused by bypassing embedding processing")
log.debug(e)
pass
# Remove the file's collection from vector database
file_collection = f"file-{form_data.file_id}"
if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
try:
# Remove the file's collection from vector database
file_collection = f"file-{form_data.file_id}"
if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
except Exception as e:
log.debug("This was most likely caused by bypassing embedding processing")
log.debug(e)
pass
# Delete file from database
Files.delete_file_by_id(form_data.file_id)
@@ -460,7 +551,7 @@ def remove_file_from_knowledge_by_id(
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -656,7 +747,7 @@ def add_files_to_knowledge_batch(
error_details = [f"{err.file_id}: {err.error}" for err in result.errors]
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=Files.get_files_by_ids(existing_file_ids),
files=Files.get_file_metadatas_by_ids(existing_file_ids),
warnings={
"message": "Some files failed to process",
"errors": error_details,
@@ -664,5 +755,6 @@ def add_files_to_knowledge_batch(
)
return KnowledgeFilesResponse(
**knowledge.model_dump(), files=Files.get_files_by_ids(existing_file_ids)
**knowledge.model_dump(),
files=Files.get_file_metadatas_by_ids(existing_file_ids),
)
+11 -5
View File
@@ -57,7 +57,9 @@ async def add_memory(
{
"id": memory.id,
"text": memory.content,
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content, user),
"vector": request.app.state.EMBEDDING_FUNCTION(
memory.content, user=user
),
"metadata": {"created_at": memory.created_at},
}
],
@@ -82,7 +84,7 @@ async def query_memory(
):
results = VECTOR_DB_CLIENT.search(
collection_name=f"user-memory-{user.id}",
vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content, user)],
vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content, user=user)],
limit=form_data.k,
)
@@ -105,7 +107,9 @@ async def reset_memory_from_vector_db(
{
"id": memory.id,
"text": memory.content,
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content, user),
"vector": request.app.state.EMBEDDING_FUNCTION(
memory.content, user=user
),
"metadata": {
"created_at": memory.created_at,
"updated_at": memory.updated_at,
@@ -149,7 +153,9 @@ async def update_memory_by_id(
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
memory = Memories.update_memory_by_id(memory_id, form_data.content)
memory = Memories.update_memory_by_id_and_user_id(
memory_id, user.id, form_data.content
)
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
@@ -161,7 +167,7 @@ async def update_memory_by_id(
"id": memory.id,
"text": memory.content,
"vector": request.app.state.EMBEDDING_FUNCTION(
memory.content, user
memory.content, user=user
),
"metadata": {
"created_at": memory.created_at,
+218
View File
@@ -0,0 +1,218 @@
import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
from pydantic import BaseModel
from open_webui.models.users import Users, UserResponse
from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# GetNotes
############################
@router.get("/", response_model=list[NoteUserResponse])
async def get_notes(request: Request, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
notes = [
NoteUserResponse(
**{
**note.model_dump(),
"user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
}
)
for note in Notes.get_notes_by_user_id(user.id, "write")
]
return notes
@router.get("/list", response_model=list[NoteUserResponse])
async def get_note_list(request: Request, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
notes = [
NoteUserResponse(
**{
**note.model_dump(),
"user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
}
)
for note in Notes.get_notes_by_user_id(user.id, "read")
]
return notes
############################
# CreateNewNote
############################
@router.post("/create", response_model=Optional[NoteModel])
async def create_new_note(
request: Request, form_data: NoteForm, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
try:
note = Notes.insert_new_note(form_data, user.id)
return note
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetNoteById
############################
@router.get("/{id}", response_model=Optional[NoteModel])
async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
note = Notes.get_note_by_id(id)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if (
user.role != "admin"
and user.id != note.user_id
and not has_access(user.id, type="read", access_control=note.access_control)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
return note
############################
# UpdateNoteById
############################
@router.post("/{id}/update", response_model=Optional[NoteModel])
async def update_note_by_id(
request: Request, id: str, form_data: NoteForm, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
note = Notes.get_note_by_id(id)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if (
user.role != "admin"
and user.id != note.user_id
and not has_access(user.id, type="write", access_control=note.access_control)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
try:
note = Notes.update_note_by_id(id, form_data)
return note
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# DeleteNoteById
############################
@router.delete("/{id}/delete", response_model=bool)
async def delete_note_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
note = Notes.get_note_by_id(id)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if (
user.role != "admin"
and user.id != note.user_id
and not has_access(user.id, type="write", access_control=note.access_control)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
try:
note = Notes.delete_note_by_id(id)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
+57 -19
View File
@@ -54,8 +54,9 @@ from open_webui.config import (
from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
BYPASS_MODEL_ACCESS_CONTROL,
)
from open_webui.constants import ERROR_MESSAGES
@@ -72,7 +73,7 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
async def send_get_request(url, key=None, user: UserModel = None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
@@ -91,6 +92,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
return await response.json()
except Exception as e:
@@ -141,6 +143,7 @@ async def send_post_request(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
r.raise_for_status()
@@ -216,7 +219,8 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@@ -234,6 +238,7 @@ async def verify_connection(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
detail = f"HTTP Error: {r.status}"
@@ -295,7 +300,7 @@ async def update_config(
}
@cached(ttl=3)
@cached(ttl=1)
async def get_all_models(request: Request, user: UserModel = None):
log.info("get_all_models()")
if request.app.state.config.ENABLE_OLLAMA_API:
@@ -336,6 +341,7 @@ async def get_all_models(request: Request, user: UserModel = None):
)
prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", [])
model_ids = api_config.get("model_ids", [])
if len(model_ids) != 0 and "models" in response:
@@ -350,6 +356,10 @@ async def get_all_models(request: Request, user: UserModel = None):
for model in response.get("models", []):
model["model"] = f"{prefix_id}.{model['model']}"
if tags:
for model in response.get("models", []):
model["tags"] = tags
def merge_models_lists(model_lists):
merged_models = {}
@@ -460,18 +470,27 @@ async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
if request.app.state.config.ENABLE_OLLAMA_API:
if url_idx is None:
# returns lowest version
request_tasks = [
send_get_request(
f"{url}/api/version",
request_tasks = []
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(
url, {}
), # Legacy support
).get("key", None),
url, {}
), # Legacy support
)
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
]
enable = api_config.get("enable", True)
key = api_config.get("key", None)
if enable:
request_tasks.append(
send_get_request(
f"{url}/api/version",
key,
)
)
responses = await asyncio.gather(*request_tasks)
responses = list(filter(lambda x: x is not None, responses))
@@ -864,8 +883,16 @@ async def embed(
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
)
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
form_data.model = form_data.model.replace(f"{prefix_id}.", "")
try:
r = requests.request(
method="POST",
@@ -943,8 +970,16 @@ async def embeddings(
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
)
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
form_data.model = form_data.model.replace(f"{prefix_id}.", "")
try:
r = requests.request(
method="POST",
@@ -992,7 +1027,7 @@ class GenerateCompletionForm(BaseModel):
prompt: str
suffix: Optional[str] = None
images: Optional[list[str]] = None
format: Optional[str] = None
format: Optional[Union[dict, str]] = None
options: Optional[dict] = None
system: Optional[str] = None
template: Optional[str] = None
@@ -1164,7 +1199,7 @@ async def generate_chat_completion(
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
# payload["keep_alive"] = -1 # keep alive forever
return await send_post_request(
url=f"{url}/api/chat",
payload=json.dumps(payload),
@@ -1183,7 +1218,7 @@ class OpenAIChatMessageContent(BaseModel):
class OpenAIChatMessage(BaseModel):
role: str
content: Union[str, list[OpenAIChatMessageContent]]
content: Union[Optional[str], list[OpenAIChatMessageContent]]
model_config = ConfigDict(extra="allow")
@@ -1468,7 +1503,9 @@ async def download_file_stream(
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(file_url, headers=headers) as response:
async with session.get(
file_url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as response:
total_size = int(response.headers.get("content-length", 0)) + current_size
with open(file_path, "ab+") as file:
@@ -1483,7 +1520,8 @@ async def download_file_stream(
if done:
file.seek(0)
hashed = calculate_sha256(file)
chunk_size = 1024 * 1024 * 2
hashed = calculate_sha256(file, chunk_size)
file.seek(0)
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
+54 -30
View File
@@ -21,8 +21,9 @@ from open_webui.config import (
CACHE_DIR,
)
from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
ENABLE_FORWARD_USER_INFO_HEADERS,
BYPASS_MODEL_ACCESS_CONTROL,
)
@@ -36,6 +37,9 @@ from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from open_webui.utils.misc import (
convert_logit_bias_input_to_json,
)
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access
@@ -53,7 +57,7 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
async def send_get_request(url, key=None, user: UserModel = None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
@@ -71,6 +75,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
return await response.json()
except Exception as e:
@@ -89,20 +94,19 @@ async def cleanup_response(
await session.close()
def openai_o1_o3_handler(payload):
def openai_o_series_handler(payload):
"""
Handle o1, o3 specific parameters
Handle "o" series specific parameters
"""
if "max_tokens" in payload:
# Remove "max_tokens" from the payload
# Convert "max_tokens" to "max_completion_tokens" for all o-series models
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
# Fix: o1 and o3 do not support the "system" role directly.
# For older models like "o1-mini" or "o1-preview", use role "user".
# For newer o1/o3 models, replace "system" with "developer".
# Handle system role conversion based on model type
if payload["messages"][0]["role"] == "system":
model_lower = payload["model"].lower()
# Legacy models use "user" role instead of "system"
if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"):
payload["messages"][0]["role"] = "user"
else:
@@ -192,7 +196,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
body = await request.body()
name = hashlib.sha256(body).hexdigest()
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
@@ -350,6 +354,7 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
)
prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", [])
if prefix_id:
for model in (
@@ -357,6 +362,12 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
):
model["id"] = f"{prefix_id}.{model['id']}"
if tags:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
model["tags"] = tags
log.debug(f"get_all_models:responses() {responses}")
return responses
@@ -374,7 +385,7 @@ async def get_filtered_models(models, user):
return filtered_models
@cached(ttl=3)
@cached(ttl=1)
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
log.info("get_all_models()")
@@ -396,6 +407,7 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
for idx, models in enumerate(model_lists):
if models is not None and "error" not in models:
merged_list.extend(
[
{
@@ -406,18 +418,21 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
"urlIdx": idx,
}
for model in models
if "api.openai.com"
not in request.app.state.config.OPENAI_API_BASE_URLS[idx]
or not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
if (model.get("id") or model.get("name"))
and (
"api.openai.com"
not in request.app.state.config.OPENAI_API_BASE_URLS[idx]
or not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
)
]
)
@@ -448,9 +463,8 @@ async def get_models(
r = None
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(
total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@@ -469,6 +483,7 @@ async def get_models(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
@@ -530,7 +545,8 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@@ -549,6 +565,7 @@ async def verify_connection(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
@@ -654,10 +671,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,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)
# Check if model is from "o" series
is_o_series = payload["model"].lower().startswith(("o1", "o3", "o4"))
if is_o_series:
payload = openai_o_series_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:
@@ -668,6 +685,11 @@ async def generate_chat_completion(
del payload["max_tokens"]
# Convert the modified body back to JSON
if "logit_bias" in payload:
payload["logit_bias"] = json.loads(
convert_logit_bias_input_to_json(payload["logit_bias"])
)
payload = json.dumps(payload)
r = None
@@ -706,6 +728,7 @@ async def generate_chat_completion(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
# Check if response is SSE
@@ -785,6 +808,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
r.raise_for_status()
+4 -4
View File
@@ -66,7 +66,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
if "pipeline" in model:
sorted_filters.append(model)
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
@@ -90,8 +90,8 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
headers=headers,
json=request_data,
) as response:
response.raise_for_status()
payload = await response.json()
response.raise_for_status()
except aiohttp.ClientResponseError as e:
res = (
await response.json()
@@ -115,7 +115,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
if "pipeline" in model:
sorted_filters = [model] + sorted_filters
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
@@ -139,8 +139,8 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
headers=headers,
json=request_data,
) as response:
response.raise_for_status()
payload = await response.json()
response.raise_for_status()
except aiohttp.ClientResponseError as e:
try:
res = (
File diff suppressed because it is too large Load Diff
+2 -24
View File
@@ -186,20 +186,9 @@ async def generate_title(
else:
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
messages = form_data["messages"]
# Remove reasoning details from the messages
for message in messages:
message["content"] = re.sub(
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
"",
message["content"],
flags=re.S,
).strip()
content = title_generation_template(
template,
messages,
form_data["messages"],
{
"name": user.name,
"location": user.info.get("location") if user.info else None,
@@ -653,17 +642,6 @@ async def generate_moa_response(
detail="Model not found",
)
# Check if the user has a custom task model
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
models,
)
log.debug(f"generating MOA model {task_model_id} for user {user.email} ")
template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE
content = moa_response_generation_template(
@@ -673,7 +651,7 @@ async def generate_moa_response(
)
payload = {
"model": task_model_id,
"model": model_id,
"messages": [{"role": "user", "content": content}],
"stream": form_data.get("stream", False),
"metadata": {
+61 -20
View File
@@ -1,6 +1,7 @@
import logging
from pathlib import Path
from typing import Optional
import time
from open_webui.models.tools import (
ToolForm,
@@ -9,15 +10,17 @@ from open_webui.models.tools import (
ToolUserResponse,
Tools,
)
from open_webui.utils.plugin import load_tools_module_by_id, replace_imports
from open_webui.utils.plugin import load_tool_module_by_id, replace_imports
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.tools import get_tools_specs
from open_webui.utils.tools import get_tool_specs
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.tools import get_tool_servers_data
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
@@ -30,11 +33,51 @@ router = APIRouter()
@router.get("/", response_model=list[ToolUserResponse])
async def get_tools(user=Depends(get_verified_user)):
if user.role == "admin":
tools = Tools.get_tools()
else:
tools = Tools.get_tools_by_user_id(user.id, "read")
async def get_tools(request: Request, user=Depends(get_verified_user)):
if not request.app.state.TOOL_SERVERS:
# If the tool servers are not set, we need to set them
# This is done only once when the server starts
# This is done to avoid loading the tool servers every time
request.app.state.TOOL_SERVERS = await get_tool_servers_data(
request.app.state.config.TOOL_SERVER_CONNECTIONS
)
tools = Tools.get_tools()
for server in request.app.state.TOOL_SERVERS:
tools.append(
ToolUserResponse(
**{
"id": f"server:{server['idx']}",
"user_id": f"server:{server['idx']}",
"name": server["openapi"]
.get("info", {})
.get("title", "Tool Server"),
"meta": {
"description": server["openapi"]
.get("info", {})
.get("description", ""),
},
"access_control": request.app.state.config.TOOL_SERVER_CONNECTIONS[
server["idx"]
]
.get("config", {})
.get("access_control", None),
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
)
if user.role != "admin":
tools = [
tool
for tool in tools
if tool.user_id == user.id
or has_access(user.id, "read", tool.access_control)
]
return tools
@@ -94,18 +137,18 @@ async def create_new_tools(
if tools is None:
try:
form_data.content = replace_imports(form_data.content)
tools_module, frontmatter = load_tools_module_by_id(
tool_module, frontmatter = load_tool_module_by_id(
form_data.id, content=form_data.content
)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[form_data.id] = tools_module
TOOLS[form_data.id] = tool_module
specs = get_tools_specs(TOOLS[form_data.id])
specs = get_tool_specs(TOOLS[form_data.id])
tools = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
tool_cache_dir.mkdir(parents=True, exist_ok=True)
if tools:
@@ -183,15 +226,13 @@ async def update_tools_by_id(
try:
form_data.content = replace_imports(form_data.content)
tools_module, frontmatter = load_tools_module_by_id(
id, content=form_data.content
)
tool_module, frontmatter = load_tool_module_by_id(id, content=form_data.content)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[id] = tools_module
TOOLS[id] = tool_module
specs = get_tools_specs(TOOLS[id])
specs = get_tool_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
@@ -289,7 +330,7 @@ async def get_tools_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "Valves"):
@@ -332,7 +373,7 @@ async def update_tools_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if not hasattr(tools_module, "Valves"):
@@ -388,7 +429,7 @@ async def get_tools_user_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
@@ -412,7 +453,7 @@ async def update_tools_user_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
+109 -11
View File
@@ -2,9 +2,11 @@ import logging
from typing import Optional
from open_webui.models.auths import Auths
from open_webui.models.groups import Groups
from open_webui.models.chats import Chats
from open_webui.models.users import (
UserModel,
UserListResponse,
UserRoleUpdateForm,
Users,
UserSettings,
@@ -17,7 +19,10 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user
from open_webui.utils.access_control import get_permissions, has_permission
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -29,13 +34,38 @@ router = APIRouter()
############################
@router.get("/", response_model=list[UserModel])
PAGE_ITEM_COUNT = 30
@router.get("/", response_model=UserListResponse)
async def get_users(
skip: Optional[int] = None,
limit: Optional[int] = None,
query: Optional[str] = None,
order_by: Optional[str] = None,
direction: Optional[str] = None,
page: Optional[int] = 1,
user=Depends(get_admin_user),
):
return Users.get_users(skip, limit)
limit = PAGE_ITEM_COUNT
page = max(1, page)
skip = (page - 1) * limit
filter = {}
if query:
filter["query"] = query
if order_by:
filter["order_by"] = order_by
if direction:
filter["direction"] = direction
return Users.get_users(filter=filter, skip=skip, limit=limit)
@router.get("/all", response_model=UserListResponse)
async def get_all_users(
user=Depends(get_admin_user),
):
return Users.get_users()
############################
@@ -45,7 +75,7 @@ async def get_users(
@router.get("/groups")
async def get_user_groups(user=Depends(get_verified_user)):
return Users.get_user_groups(user.id)
return Groups.get_groups_by_member_id(user.id)
############################
@@ -54,8 +84,12 @@ async def get_user_groups(user=Depends(get_verified_user)):
@router.get("/permissions")
async def get_user_permissisions(user=Depends(get_verified_user)):
return Users.get_user_groups(user.id)
async def get_user_permissisions(request: Request, user=Depends(get_verified_user)):
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
)
return user_permissions
############################
@@ -68,32 +102,52 @@ class WorkspacePermissions(BaseModel):
tools: bool = False
class SharingPermissions(BaseModel):
public_models: bool = True
public_knowledge: bool = True
public_prompts: bool = True
public_tools: bool = True
class ChatPermissions(BaseModel):
controls: bool = True
file_upload: bool = True
delete: bool = True
edit: bool = True
share: bool = True
export: bool = True
stt: bool = True
tts: bool = True
call: bool = True
multiple_models: bool = True
temporary: bool = True
temporary_enforced: bool = False
class FeaturesPermissions(BaseModel):
direct_tool_servers: bool = False
web_search: bool = True
image_generation: bool = True
code_interpreter: bool = True
notes: bool = True
class UserPermissions(BaseModel):
workspace: WorkspacePermissions
sharing: SharingPermissions
chat: ChatPermissions
features: FeaturesPermissions
@router.get("/default/permissions", response_model=UserPermissions)
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)):
return {
"workspace": WorkspacePermissions(
**request.app.state.config.USER_PERMISSIONS.get("workspace", {})
),
"sharing": SharingPermissions(
**request.app.state.config.USER_PERMISSIONS.get("sharing", {})
),
"chat": ChatPermissions(
**request.app.state.config.USER_PERMISSIONS.get("chat", {})
),
@@ -104,7 +158,7 @@ async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
@router.post("/default/permissions")
async def update_user_permissions(
async def update_default_user_permissions(
request: Request, form_data: UserPermissions, user=Depends(get_admin_user)
):
request.app.state.config.USER_PERMISSIONS = form_data.model_dump()
@@ -151,9 +205,22 @@ async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
@router.post("/user/settings/update", response_model=UserSettings)
async def update_user_settings_by_session_user(
form_data: UserSettings, user=Depends(get_verified_user)
request: Request, form_data: UserSettings, user=Depends(get_verified_user)
):
user = Users.update_user_settings_by_id(user.id, form_data.model_dump())
updated_user_settings = form_data.model_dump()
if (
user.role != "admin"
and "toolServers" in updated_user_settings.get("ui").keys()
and not has_permission(
user.id,
"features.direct_tool_servers",
request.app.state.config.USER_PERMISSIONS,
)
):
# If the user is not an admin and does not have permission to use tool servers, remove the key
updated_user_settings["ui"].pop("toolServers", None)
user = Users.update_user_settings_by_id(user.id, updated_user_settings)
if user:
return user.settings
else:
@@ -263,6 +330,21 @@ async def update_user_by_id(
form_data: UserUpdateForm,
session_user=Depends(get_admin_user),
):
# Prevent modification of the primary admin user by other admins
try:
first_user = Users.get_first_user()
if first_user and user_id == first_user.id and session_user.id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
)
user = Users.get_user_by_id(user_id)
if user:
@@ -310,6 +392,21 @@ async def update_user_by_id(
@router.delete("/{user_id}", response_model=bool)
async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
# Prevent deletion of the primary admin user
try:
first_user = Users.get_first_user()
if first_user and user_id == first_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
)
if user.id != user_id:
result = Auths.delete_auth_by_id(user_id)
@@ -321,6 +418,7 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
detail=ERROR_MESSAGES.DELETE_USER_ERROR,
)
# Prevent self-deletion
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
+96 -49
View File
@@ -3,15 +3,23 @@ import socketio
import logging
import sys
import time
from redis import asyncio as aioredis
from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels
from open_webui.models.chats import Chats
from open_webui.utils.redis import (
get_sentinels_from_env,
get_sentinel_url_from_env,
)
from open_webui.env import (
ENABLE_WEBSOCKET_SUPPORT,
WEBSOCKET_MANAGER,
WEBSOCKET_REDIS_URL,
WEBSOCKET_REDIS_LOCK_TIMEOUT,
WEBSOCKET_SENTINEL_PORT,
WEBSOCKET_SENTINEL_HOSTS,
)
from open_webui.utils.auth import decode_token
from open_webui.socket.utils import RedisDict, RedisLock
@@ -28,7 +36,14 @@ log.setLevel(SRC_LOG_LEVELS["SOCKET"])
if WEBSOCKET_MANAGER == "redis":
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
if WEBSOCKET_SENTINEL_HOSTS:
mgr = socketio.AsyncRedisManager(
get_sentinel_url_from_env(
WEBSOCKET_REDIS_URL, WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
)
)
else:
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
sio = socketio.AsyncServer(
cors_allowed_origins=[],
async_mode="asgi",
@@ -54,14 +69,30 @@ TIMEOUT_DURATION = 3
if WEBSOCKET_MANAGER == "redis":
log.debug("Using Redis to manage websockets.")
SESSION_POOL = RedisDict("open-webui:session_pool", redis_url=WEBSOCKET_REDIS_URL)
USER_POOL = RedisDict("open-webui:user_pool", redis_url=WEBSOCKET_REDIS_URL)
USAGE_POOL = RedisDict("open-webui:usage_pool", redis_url=WEBSOCKET_REDIS_URL)
redis_sentinels = get_sentinels_from_env(
WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
)
SESSION_POOL = RedisDict(
"open-webui:session_pool",
redis_url=WEBSOCKET_REDIS_URL,
redis_sentinels=redis_sentinels,
)
USER_POOL = RedisDict(
"open-webui:user_pool",
redis_url=WEBSOCKET_REDIS_URL,
redis_sentinels=redis_sentinels,
)
USAGE_POOL = RedisDict(
"open-webui:usage_pool",
redis_url=WEBSOCKET_REDIS_URL,
redis_sentinels=redis_sentinels,
)
clean_up_lock = RedisLock(
redis_url=WEBSOCKET_REDIS_URL,
lock_name="usage_cleanup_lock",
timeout_secs=TIMEOUT_DURATION * 2,
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
redis_sentinels=redis_sentinels,
)
aquire_func = clean_up_lock.aquire_lock
renew_func = clean_up_lock.renew_lock
@@ -128,18 +159,19 @@ def get_models_in_use():
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
if sid in SESSION_POOL:
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
@sio.event
@@ -247,7 +279,8 @@ async def channel_events(sid, data):
@sio.on("user-list")
async def user_list(sid):
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
if sid in SESSION_POOL:
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
@sio.event
@@ -268,15 +301,23 @@ async def disconnect(sid):
# print(f"Unknown session ID {sid} disconnected")
def get_event_emitter(request_info):
def get_event_emitter(request_info, update_db=True):
async def __event_emitter__(event_data):
user_id = request_info["user_id"]
session_ids = list(
set(USER_POOL.get(user_id, []) + [request_info["session_id"]])
set(
USER_POOL.get(user_id, [])
+ (
[request_info.get("session_id")]
if request_info.get("session_id")
else []
)
)
)
for session_id in session_ids:
await sio.emit(
emit_tasks = [
sio.emit(
"chat-events",
{
"chat_id": request_info.get("chat_id", None),
@@ -285,41 +326,47 @@ def get_event_emitter(request_info):
},
to=session_id,
)
for session_id in session_ids
]
if "type" in event_data and event_data["type"] == "status":
Chats.add_message_status_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
event_data.get("data", {}),
)
await asyncio.gather(*emit_tasks)
if "type" in event_data and event_data["type"] == "message":
message = Chats.get_message_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
)
if update_db:
if "type" in event_data and event_data["type"] == "status":
Chats.add_message_status_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
event_data.get("data", {}),
)
content = message.get("content", "")
content += event_data.get("data", {}).get("content", "")
if "type" in event_data and event_data["type"] == "message":
message = Chats.get_message_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
)
Chats.upsert_message_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
{
"content": content,
},
)
if message:
content = message.get("content", "")
content += event_data.get("data", {}).get("content", "")
if "type" in event_data and event_data["type"] == "replace":
content = event_data.get("data", {}).get("content", "")
Chats.upsert_message_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
{
"content": content,
},
)
Chats.upsert_message_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
{
"content": content,
},
)
if "type" in event_data and event_data["type"] == "replace":
content = event_data.get("data", {}).get("content", "")
Chats.upsert_message_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
{
"content": content,
},
)
return __event_emitter__
+9 -5
View File
@@ -1,15 +1,17 @@
import json
import redis
import uuid
from open_webui.utils.redis import get_redis_connection
class RedisLock:
def __init__(self, redis_url, lock_name, timeout_secs):
def __init__(self, redis_url, lock_name, timeout_secs, redis_sentinels=[]):
self.lock_name = lock_name
self.lock_id = str(uuid.uuid4())
self.timeout_secs = timeout_secs
self.lock_obtained = False
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
self.redis = get_redis_connection(
redis_url, redis_sentinels, decode_responses=True
)
def aquire_lock(self):
# nx=True will only set this key if it _hasn't_ already been set
@@ -31,9 +33,11 @@ class RedisLock:
class RedisDict:
def __init__(self, name, redis_url):
def __init__(self, name, redis_url, redis_sentinels=[]):
self.name = name
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
self.redis = get_redis_connection(
redis_url, redis_sentinels, decode_responses=True
)
def __setitem__(self, key, value):
serialized_value = json.dumps(value)
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

@@ -3,13 +3,13 @@
"short_name": "WebUI",
"icons": [
{
"src": "/favicon/web-app-manifest-192x192.png",
"src": "/static/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/favicon/web-app-manifest-512x512.png",
"src": "/static/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+55 -23
View File
@@ -3,7 +3,7 @@ import shutil
import json
import logging
from abc import ABC, abstractmethod
from typing import BinaryIO, Tuple
from typing import BinaryIO, Tuple, Dict
import boto3
from botocore.config import Config
@@ -17,6 +17,7 @@ from open_webui.config import (
S3_SECRET_ACCESS_KEY,
S3_USE_ACCELERATE_ENDPOINT,
S3_ADDRESSING_STYLE,
S3_ENABLE_TAGGING,
GCS_BUCKET_NAME,
GOOGLE_APPLICATION_CREDENTIALS_JSON,
AZURE_STORAGE_ENDPOINT,
@@ -44,7 +45,9 @@ class StorageProvider(ABC):
pass
@abstractmethod
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
pass
@abstractmethod
@@ -58,7 +61,9 @@ class StorageProvider(ABC):
class LocalStorageProvider(StorageProvider):
@staticmethod
def upload_file(file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
contents = file.read()
if not contents:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
@@ -101,31 +106,54 @@ class LocalStorageProvider(StorageProvider):
class S3StorageProvider(StorageProvider):
def __init__(self):
self.s3_client = boto3.client(
"s3",
region_name=S3_REGION_NAME,
endpoint_url=S3_ENDPOINT_URL,
aws_access_key_id=S3_ACCESS_KEY_ID,
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
config=Config(
s3={
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
"addressing_style": S3_ADDRESSING_STYLE,
},
),
config = Config(
s3={
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
"addressing_style": S3_ADDRESSING_STYLE,
},
)
# If access key and secret are provided, use them for authentication
if S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY:
self.s3_client = boto3.client(
"s3",
region_name=S3_REGION_NAME,
endpoint_url=S3_ENDPOINT_URL,
aws_access_key_id=S3_ACCESS_KEY_ID,
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
config=config,
)
else:
# If no explicit credentials are provided, fall back to default AWS credentials
# This supports workload identity (IAM roles for EC2, EKS, etc.)
self.s3_client = boto3.client(
"s3",
region_name=S3_REGION_NAME,
endpoint_url=S3_ENDPOINT_URL,
config=config,
)
self.bucket_name = S3_BUCKET_NAME
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to S3 storage."""
_, file_path = LocalStorageProvider.upload_file(file, filename)
_, file_path = LocalStorageProvider.upload_file(file, filename, tags)
s3_key = os.path.join(self.key_prefix, filename)
try:
s3_key = os.path.join(self.key_prefix, filename)
self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
if S3_ENABLE_TAGGING and tags:
tagging = {"TagSet": [{"Key": k, "Value": v} for k, v in tags.items()]}
self.s3_client.put_object_tagging(
Bucket=self.bucket_name,
Key=s3_key,
Tagging=tagging,
)
return (
open(file_path, "rb").read(),
"s3://" + self.bucket_name + "/" + s3_key,
f"s3://{self.bucket_name}/{s3_key}",
)
except ClientError as e:
raise RuntimeError(f"Error uploading file to S3: {e}")
@@ -193,9 +221,11 @@ class GCSStorageProvider(StorageProvider):
self.gcs_client = storage.Client()
self.bucket = self.gcs_client.bucket(GCS_BUCKET_NAME)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to GCS storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
contents, file_path = LocalStorageProvider.upload_file(file, filename, tags)
try:
blob = self.bucket.blob(filename)
blob.upload_from_filename(file_path)
@@ -263,9 +293,11 @@ class AzureStorageProvider(StorageProvider):
self.container_name
)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to Azure Blob Storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
contents, file_path = LocalStorageProvider.upload_file(file, filename, tags)
try:
blob_client = self.container_client.get_blob_client(filename)
blob_client.upload_blob(contents, overwrite=True)
+24 -4
View File
@@ -5,16 +5,23 @@ from uuid import uuid4
# A dictionary to keep track of active tasks
tasks: Dict[str, asyncio.Task] = {}
chat_tasks = {}
def cleanup_task(task_id: str):
def cleanup_task(task_id: str, id=None):
"""
Remove a completed or canceled task from the global `tasks` dictionary.
"""
tasks.pop(task_id, None) # Remove the task if it exists
# If an ID is provided, remove the task from the chat_tasks dictionary
if id and task_id in chat_tasks.get(id, []):
chat_tasks[id].remove(task_id)
if not chat_tasks[id]: # If no tasks left for this ID, remove the entry
chat_tasks.pop(id, None)
def create_task(coroutine):
def create_task(coroutine, id=None):
"""
Create a new asyncio task and add it to the global task dictionary.
"""
@@ -22,9 +29,15 @@ def create_task(coroutine):
task = asyncio.create_task(coroutine) # Create the task
# Add a done callback for cleanup
task.add_done_callback(lambda t: cleanup_task(task_id))
task.add_done_callback(lambda t: cleanup_task(task_id, id))
tasks[task_id] = task
# If an ID is provided, associate the task with that ID
if chat_tasks.get(id):
chat_tasks[id].append(task_id)
else:
chat_tasks[id] = [task_id]
return task_id, task
@@ -42,6 +55,13 @@ def list_tasks():
return list(tasks.keys())
def list_task_ids_by_chat_id(id):
"""
List all tasks associated with a specific ID.
"""
return chat_tasks.get(id, [])
async def stop_task(task_id: str):
"""
Cancel a running task and remove it from the global task list.
@@ -187,6 +187,17 @@ class TestS3StorageProvider:
assert not (upload_dir / self.filename).exists()
assert not (upload_dir / self.filename_extra).exists()
def test_init_without_credentials(self, monkeypatch):
"""Test that S3StorageProvider can initialize without explicit credentials."""
# Temporarily unset the environment variables
monkeypatch.setattr(provider, "S3_ACCESS_KEY_ID", None)
monkeypatch.setattr(provider, "S3_SECRET_ACCESS_KEY", None)
# Should not raise an exception
storage = provider.S3StorageProvider()
assert storage.s3_client is not None
assert storage.bucket_name == provider.S3_BUCKET_NAME
class TestGCSStorageProvider:
Storage = provider.GCSStorageProvider()
+44 -10
View File
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
class AuditLogEntry:
# `Metadata` audit level properties
id: str
user: dict[str, Any]
user: Optional[dict[str, Any]]
audit_level: str
verb: str
request_uri: str
@@ -190,21 +190,40 @@ class AuditLoggingMiddleware:
finally:
await self._log_audit_entry(request, context)
async def _get_authenticated_user(self, request: Request) -> UserModel:
async def _get_authenticated_user(self, request: Request) -> Optional[UserModel]:
auth_header = request.headers.get("Authorization")
assert auth_header
user = get_current_user(request, None, get_http_authorization_cred(auth_header))
return user
try:
user = get_current_user(
request, None, get_http_authorization_cred(auth_header)
)
return user
except Exception as e:
logger.debug(f"Failed to get authenticated user: {str(e)}")
return None
def _should_skip_auditing(self, request: Request) -> bool:
if (
request.method not in {"POST", "PUT", "PATCH", "DELETE"}
or AUDIT_LOG_LEVEL == "NONE"
or not request.headers.get("authorization")
):
return True
ALWAYS_LOG_ENDPOINTS = {
"/api/v1/auths/signin",
"/api/v1/auths/signout",
"/api/v1/auths/signup",
}
path = request.url.path.lower()
for endpoint in ALWAYS_LOG_ENDPOINTS:
if path.startswith(endpoint):
return False # Do NOT skip logging for auth endpoints
# Skip logging if the request is not authenticated
if not request.headers.get("authorization"):
return True
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
pattern = re.compile(
r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b"
@@ -231,17 +250,32 @@ class AuditLoggingMiddleware:
try:
user = await self._get_authenticated_user(request)
user = (
user.model_dump(include={"id", "name", "email", "role"}) if user else {}
)
request_body = context.request_body.decode("utf-8", errors="replace")
response_body = context.response_body.decode("utf-8", errors="replace")
# Redact sensitive information
if "password" in request_body:
request_body = re.sub(
r'"password":\s*"(.*?)"',
'"password": "********"',
request_body,
)
entry = AuditLogEntry(
id=str(uuid.uuid4()),
user=user.model_dump(include={"id", "name", "email", "role"}),
user=user,
audit_level=self.audit_level.value,
verb=request.method,
request_uri=str(request.url),
response_status_code=context.metadata.get("response_status_code", None),
source_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
request_object=context.request_body.decode("utf-8", errors="replace"),
response_object=context.response_body.decode("utf-8", errors="replace"),
request_object=request_body,
response_object=response_body,
)
self.audit_logger.write(entry)
+18 -8
View File
@@ -8,7 +8,9 @@ import requests
import os
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
import pytz
from pytz import UTC
from typing import Optional, Union, List, Dict
from open_webui.models.users import Users
@@ -72,7 +74,7 @@ def get_license_data(app, key):
if key:
try:
res = requests.post(
"https://api.openwebui.com/api/v1/license",
"https://api.openwebui.com/api/v1/license/",
json={"key": key, "version": "1"},
timeout=5,
)
@@ -83,11 +85,12 @@ def get_license_data(app, key):
if k == "resources":
for p, c in v.items():
globals().get("override_static", lambda a, b: None)(p, c)
elif k == "user_count":
elif k == "count":
setattr(app.state, "USER_COUNT", v)
elif k == "webui_name":
elif k == "name":
setattr(app.state, "WEBUI_NAME", v)
elif k == "metadata":
setattr(app.state, "LICENSE_METADATA", v)
return True
else:
log.error(
@@ -140,12 +143,14 @@ def create_api_key():
return f"sk-{key}"
def get_http_authorization_cred(auth_header: str):
def get_http_authorization_cred(auth_header: Optional[str]):
if not auth_header:
return None
try:
scheme, credentials = auth_header.split(" ")
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
except Exception:
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
return None
def get_current_user(
@@ -179,7 +184,12 @@ def get_current_user(
).split(",")
]
if request.url.path not in allowed_paths:
# Check if the request path matches any allowed endpoint.
if not any(
request.url.path == allowed
or request.url.path.startswith(allowed + "/")
for allowed in allowed_paths
):
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED
)
+7 -2
View File
@@ -149,7 +149,7 @@ async def generate_direct_chat_completion(
}
)
if "error" in res:
if "error" in res and res["error"]:
raise Exception(res["error"])
return res
@@ -328,9 +328,14 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
}
try:
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model)
]
result, _ = await process_filter_functions(
request=request,
filter_ids=get_sorted_filter_ids(model),
filter_functions=filter_functions,
filter_type="outlet",
form_data=data,
extra_params=extra_params,
+188 -126
View File
@@ -1,148 +1,210 @@
import asyncio
import json
import logging
import uuid
from typing import Optional
import aiohttp
import websockets
import requests
from urllib.parse import urljoin
from pydantic import BaseModel
from open_webui.env import SRC_LOG_LEVELS
logger = logging.getLogger(__name__)
logger.setLevel(SRC_LOG_LEVELS["MAIN"])
async def execute_code_jupyter(
jupyter_url, code, token=None, password=None, timeout=10
):
class ResultModel(BaseModel):
"""
Executes Python code in a Jupyter kernel.
Supports authentication with a token or password.
:param jupyter_url: Jupyter server URL (e.g., "http://localhost:8888")
:param code: Code to execute
:param token: Jupyter authentication token (optional)
:param password: Jupyter password (optional)
:param timeout: WebSocket timeout in seconds (default: 10s)
:return: Dictionary with stdout, stderr, and result
- Images are prefixed with "base64:image/png," and separated by newlines if multiple.
Execute Code Result Model
"""
session = requests.Session() # Maintain cookies
headers = {} # Headers for requests
# Authenticate using password
if password and not token:
stdout: Optional[str] = ""
stderr: Optional[str] = ""
result: Optional[str] = ""
class JupyterCodeExecuter:
"""
Execute code in jupyter notebook
"""
def __init__(
self,
base_url: str,
code: str,
token: str = "",
password: str = "",
timeout: int = 60,
):
"""
:param base_url: Jupyter server URL (e.g., "http://localhost:8888")
:param code: Code to execute
:param token: Jupyter authentication token (optional)
:param password: Jupyter password (optional)
:param timeout: WebSocket timeout in seconds (default: 60s)
"""
self.base_url = base_url
self.code = code
self.token = token
self.password = password
self.timeout = timeout
self.kernel_id = ""
if self.base_url[-1] != "/":
self.base_url += "/"
self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url)
self.params = {}
self.result = ResultModel()
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.kernel_id:
try:
async with self.session.delete(
f"api/kernels/{self.kernel_id}", params=self.params
) as response:
response.raise_for_status()
except Exception as err:
logger.exception("close kernel failed, %s", err)
await self.session.close()
async def run(self) -> ResultModel:
try:
login_url = urljoin(jupyter_url, "/login")
response = session.get(login_url)
await self.sign_in()
await self.init_kernel()
await self.execute_code()
except Exception as err:
logger.exception("execute code failed, %s", err)
self.result.stderr = f"Error: {err}"
return self.result
async def sign_in(self) -> None:
# password authentication
if self.password and not self.token:
async with self.session.get("login") as response:
response.raise_for_status()
xsrf_token = response.cookies["_xsrf"].value
if not xsrf_token:
raise ValueError("_xsrf token not found")
self.session.cookie_jar.update_cookies(response.cookies)
self.session.headers.update({"X-XSRFToken": xsrf_token})
async with self.session.post(
"login",
data={"_xsrf": xsrf_token, "password": self.password},
allow_redirects=False,
) as response:
response.raise_for_status()
self.session.cookie_jar.update_cookies(response.cookies)
# token authentication
if self.token:
self.params.update({"token": self.token})
async def init_kernel(self) -> None:
async with self.session.post(url="api/kernels", params=self.params) as response:
response.raise_for_status()
xsrf_token = session.cookies.get("_xsrf")
if not xsrf_token:
raise ValueError("Failed to fetch _xsrf token")
login_data = {"_xsrf": xsrf_token, "password": password}
login_response = session.post(
login_url, data=login_data, cookies=session.cookies
)
login_response.raise_for_status()
headers["X-XSRFToken"] = xsrf_token
except Exception as e:
return {
"stdout": "",
"stderr": f"Authentication Error: {str(e)}",
"result": "",
}
# Construct API URLs with authentication token if provided
params = f"?token={token}" if token else ""
kernel_url = urljoin(jupyter_url, f"/api/kernels{params}")
try:
response = session.post(kernel_url, headers=headers, cookies=session.cookies)
response.raise_for_status()
kernel_id = response.json()["id"]
websocket_url = urljoin(
jupyter_url.replace("http", "ws"),
f"/api/kernels/{kernel_id}/channels{params}",
)
kernel_data = await response.json()
self.kernel_id = kernel_data["id"]
def init_ws(self) -> (str, dict):
ws_base = self.base_url.replace("http", "ws", 1)
ws_params = "?" + "&".join([f"{key}={val}" for key, val in self.params.items()])
websocket_url = f"{ws_base}api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}"
ws_headers = {}
if password and not token:
ws_headers["X-XSRFToken"] = session.cookies.get("_xsrf")
cookies = {name: value for name, value in session.cookies.items()}
ws_headers["Cookie"] = "; ".join(
[f"{name}={value}" for name, value in cookies.items()]
)
if self.password and not self.token:
ws_headers = {
"Cookie": "; ".join(
[
f"{cookie.key}={cookie.value}"
for cookie in self.session.cookie_jar
]
),
**self.session.headers,
}
return websocket_url, ws_headers
async def execute_code(self) -> None:
# initialize ws
websocket_url, ws_headers = self.init_ws()
# execute
async with websockets.connect(
websocket_url, additional_headers=ws_headers
) as ws:
msg_id = str(uuid.uuid4())
execute_request = {
"header": {
"msg_id": msg_id,
"msg_type": "execute_request",
"username": "user",
"session": str(uuid.uuid4()),
"date": "",
"version": "5.3",
},
"parent_header": {},
"metadata": {},
"content": {
"code": code,
"silent": False,
"store_history": True,
"user_expressions": {},
"allow_stdin": False,
"stop_on_error": True,
},
"channel": "shell",
}
await ws.send(json.dumps(execute_request))
await self.execute_in_jupyter(ws)
stdout, stderr, result = "", "", []
while True:
try:
message = await asyncio.wait_for(ws.recv(), timeout)
message_data = json.loads(message)
if message_data.get("parent_header", {}).get("msg_id") == msg_id:
msg_type = message_data.get("msg_type")
if msg_type == "stream":
if message_data["content"]["name"] == "stdout":
stdout += message_data["content"]["text"]
elif message_data["content"]["name"] == "stderr":
stderr += message_data["content"]["text"]
elif msg_type in ("execute_result", "display_data"):
data = message_data["content"]["data"]
if "image/png" in data:
result.append(
f"data:image/png;base64,{data['image/png']}"
)
elif "text/plain" in data:
result.append(data["text/plain"])
elif msg_type == "error":
stderr += "\n".join(message_data["content"]["traceback"])
elif (
msg_type == "status"
and message_data["content"]["execution_state"] == "idle"
):
async def execute_in_jupyter(self, ws) -> None:
# send message
msg_id = uuid.uuid4().hex
await ws.send(
json.dumps(
{
"header": {
"msg_id": msg_id,
"msg_type": "execute_request",
"username": "user",
"session": uuid.uuid4().hex,
"date": "",
"version": "5.3",
},
"parent_header": {},
"metadata": {},
"content": {
"code": self.code,
"silent": False,
"store_history": True,
"user_expressions": {},
"allow_stdin": False,
"stop_on_error": True,
},
"channel": "shell",
}
)
)
# parse message
stdout, stderr, result = "", "", []
while True:
try:
# wait for message
message = await asyncio.wait_for(ws.recv(), self.timeout)
message_data = json.loads(message)
# msg id not match, skip
if message_data.get("parent_header", {}).get("msg_id") != msg_id:
continue
# check message type
msg_type = message_data.get("msg_type")
match msg_type:
case "stream":
if message_data["content"]["name"] == "stdout":
stdout += message_data["content"]["text"]
elif message_data["content"]["name"] == "stderr":
stderr += message_data["content"]["text"]
case "execute_result" | "display_data":
data = message_data["content"]["data"]
if "image/png" in data:
result.append(f"data:image/png;base64,{data['image/png']}")
elif "text/plain" in data:
result.append(data["text/plain"])
case "error":
stderr += "\n".join(message_data["content"]["traceback"])
case "status":
if message_data["content"]["execution_state"] == "idle":
break
except asyncio.TimeoutError:
stderr += "\nExecution timed out."
break
except asyncio.TimeoutError:
stderr += "\nExecution timed out."
break
self.result.stdout = stdout.strip()
self.result.stderr = stderr.strip()
self.result.result = "\n".join(result).strip() if result else ""
except Exception as e:
return {"stdout": "", "stderr": f"Error: {str(e)}", "result": ""}
finally:
if kernel_id:
requests.delete(
f"{kernel_url}/{kernel_id}", headers=headers, cookies=session.cookies
)
return {
"stdout": stdout.strip(),
"stderr": stderr.strip(),
"result": "\n".join(result).strip() if result else "",
}
async def execute_code_jupyter(
base_url: str, code: str, token: str = "", password: str = "", timeout: int = 60
) -> dict:
async with JupyterCodeExecuter(
base_url, code, token, password, timeout
) as executor:
result = await executor.run()
return result.model_dump()
+15 -13
View File
@@ -9,12 +9,12 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_sorted_filter_ids(model):
def get_sorted_filter_ids(model: dict):
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)
if function is not None:
valves = Functions.get_function_valves_by_id(function_id)
return valves.get("priority", 0) if valves else 0
return 0
filter_ids = [function.id for function in Functions.get_global_filter_functions()]
@@ -33,12 +33,13 @@ def get_sorted_filter_ids(model):
async def process_filter_functions(
request, filter_ids, filter_type, form_data, extra_params
request, filter_functions, filter_type, form_data, extra_params
):
skip_files = None
for filter_id in filter_ids:
filter = Functions.get_function_by_id(filter_id)
for function in filter_functions:
filter = function
filter_id = function.id
if not filter:
continue
@@ -48,6 +49,11 @@ async def process_filter_functions(
function_module, _, _ = load_function_module_by_id(filter_id)
request.app.state.FUNCTIONS[filter_id] = function_module
# Prepare handler function
handler = getattr(function_module, filter_type, None)
if not handler:
continue
# Check if the function has a file_handler variable
if filter_type == "inlet" and hasattr(function_module, "file_handler"):
skip_files = function_module.file_handler
@@ -59,11 +65,6 @@ async def process_filter_functions(
**(valves if valves else {})
)
# Prepare handler function
handler = getattr(function_module, filter_type, None)
if not handler:
continue
try:
# Prepare parameters
sig = inspect.signature(handler)
@@ -100,11 +101,12 @@ async def process_filter_functions(
form_data = handler(**params)
except Exception as e:
log.exception(f"Error in {filter_type} handler {filter_id}: {e}")
log.debug(f"Error in {filter_type} handler {filter_id}: {e}")
raise e
# Handle file cleanup for inlet
if skip_files and "files" in form_data.get("metadata", {}):
del form_data["files"]
del form_data["metadata"]["files"]
return form_data, {}
+442 -184
View File
@@ -18,9 +18,7 @@ from uuid import uuid4
from concurrent.futures import ThreadPoolExecutor
from fastapi import Request
from fastapi import BackgroundTasks
from fastapi import Request, HTTPException
from starlette.responses import Response, StreamingResponse
@@ -68,6 +66,7 @@ from open_webui.utils.misc import (
get_last_user_message,
get_last_assistant_message,
prepend_to_first_user_message_content,
convert_logit_bias_input_to_json,
)
from open_webui.utils.tools import get_tools
from open_webui.utils.plugin import load_function_module_by_id
@@ -99,7 +98,7 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
async def chat_completion_tools_handler(
request: Request, body: dict, user: UserModel, models, tools
request: Request, body: dict, extra_params: dict, user: UserModel, models, tools
) -> tuple[dict, dict]:
async def get_content_from_response(response) -> Optional[str]:
content = None
@@ -134,6 +133,9 @@ async def chat_completion_tools_handler(
"metadata": {"task": str(TASKS.FUNCTION_CALLING)},
}
event_caller = extra_params["__event_call__"]
metadata = extra_params["__metadata__"]
task_model_id = get_task_model_id(
body["model"],
request.app.state.config.TASK_MODEL,
@@ -155,7 +157,6 @@ async def chat_completion_tools_handler(
tools_function_calling_prompt = tools_function_calling_generation_template(
template, tools_specs
)
log.info(f"{tools_function_calling_prompt=}")
payload = get_tools_function_calling_payload(
body["messages"], task_model_id, tools_function_calling_prompt
)
@@ -188,52 +189,83 @@ async def chat_completion_tools_handler(
tool_function_params = tool_call.get("parameters", {})
try:
required_params = (
tools[tool_function_name]
.get("spec", {})
.get("parameters", {})
.get("required", [])
tool = tools[tool_function_name]
spec = tool.get("spec", {})
allowed_params = (
spec.get("parameters", {}).get("properties", {}).keys()
)
tool_function = tools[tool_function_name]["callable"]
tool_function_params = {
k: v
for k, v in tool_function_params.items()
if k in required_params
if k in allowed_params
}
tool_output = await tool_function(**tool_function_params)
except Exception as e:
tool_output = str(e)
if isinstance(tool_output, str):
if tools[tool_function_name]["citation"]:
sources.append(
if tool.get("direct", False):
tool_result = await event_caller(
{
"source": {
"name": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
"type": "execute:tool",
"data": {
"id": str(uuid4()),
"name": tool_function_name,
"params": tool_function_params,
"server": tool.get("server", {}),
"session_id": metadata.get("session_id", None),
},
"document": [tool_output],
"metadata": [
{
"source": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
}
],
}
)
else:
tool_function = tool["callable"]
tool_result = await tool_function(**tool_function_params)
except Exception as e:
tool_result = str(e)
tool_result_files = []
if isinstance(tool_result, list):
for item in tool_result:
# check if string
if isinstance(item, str) and item.startswith("data:"):
tool_result_files.append(item)
tool_result.remove(item)
if isinstance(tool_result, dict) or isinstance(tool_result, list):
tool_result = json.dumps(tool_result, indent=2)
if isinstance(tool_result, str):
tool = tools[tool_function_name]
tool_id = tool.get("tool_id", "")
tool_name = (
f"{tool_id}/{tool_function_name}"
if tool_id
else f"{tool_function_name}"
)
if tool.get("metadata", {}).get("citation", False) or tool.get(
"direct", False
):
# Citation is enabled for this tool
sources.append(
{
"source": {},
"document": [tool_output],
"metadata": [
{
"source": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
}
],
"source": {
"name": (f"TOOL:{tool_name}"),
},
"document": [tool_result],
"metadata": [{"source": (f"TOOL:{tool_name}")}],
}
)
else:
# Citation is not enabled for this tool
body["messages"] = add_or_update_user_message(
f"\nTool `{tool_name}` Output: {tool_result}",
body["messages"],
)
if tools[tool_function_name]["file_handler"]:
if (
tools[tool_function_name]
.get("metadata", {})
.get("file_handler", False)
):
skip_files = True
# check if "tool_calls" in result
@@ -244,10 +276,10 @@ async def chat_completion_tools_handler(
await tool_call_handler(result)
except Exception as e:
log.exception(f"Error: {e}")
log.debug(f"Error: {e}")
content = None
except Exception as e:
log.exception(f"Error: {e}")
log.debug(f"Error: {e}")
content = None
log.debug(f"tool_contexts: {sources}")
@@ -321,95 +353,86 @@ async def chat_web_search_handler(
)
return form_data
all_results = []
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "Searching the web",
"done": False,
},
}
)
for searchQuery in queries:
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": 'Searching "{{searchQuery}}"',
"query": searchQuery,
"done": False,
},
}
try:
results = await process_web_search(
request,
SearchForm(queries=queries),
user=user,
)
try:
results = await process_web_search(
request,
SearchForm(
**{
"query": searchQuery,
if results:
files = form_data.get("files", [])
if results.get("collection_names"):
for col_idx, collection_name in enumerate(
results.get("collection_names")
):
files.append(
{
"collection_name": collection_name,
"name": ", ".join(queries),
"type": "web_search",
"urls": results["filenames"],
}
)
elif results.get("docs"):
# Invoked when bypass embedding and retrieval is set to True
docs = results["docs"]
files.append(
{
"docs": docs,
"name": ", ".join(queries),
"type": "web_search",
"urls": results["filenames"],
}
),
user=user,
)
)
if results:
all_results.append(results)
files = form_data.get("files", [])
form_data["files"] = files
if results.get("collection_name"):
files.append(
{
"collection_name": results["collection_name"],
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
elif results.get("docs"):
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
form_data["files"] = files
except Exception as e:
log.exception(e)
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": 'Error searching "{{searchQuery}}"',
"query": searchQuery,
"description": "Searched {{count}} sites",
"urls": results["filenames"],
"done": True,
},
}
)
else:
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "No search results found",
"done": True,
"error": True,
},
}
)
if all_results:
urls = []
for results in all_results:
if "filenames" in results:
urls.extend(results["filenames"])
except Exception as e:
log.exception(e)
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "Searched {{count}} sites",
"urls": urls,
"done": True,
},
}
)
else:
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "No search results found",
"description": "An error occurred while searching the web",
"queries": queries,
"done": True,
"error": True,
},
@@ -482,13 +505,20 @@ async def chat_image_generation_handler(
}
)
for image in images:
await __event_emitter__(
{
"type": "message",
"data": {"content": f"![Generated Image]({image['url']})\n"},
}
)
await __event_emitter__(
{
"type": "files",
"data": {
"files": [
{
"type": "image",
"url": image["url"],
}
for image in images
]
},
}
)
system_message_content = "<context>User is shown the generated image, tell the user that the image has been generated</context>"
except Exception as e:
@@ -561,11 +591,12 @@ async def chat_completion_files_handler(
request=request,
files=files,
queries=queries,
embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
query, user=user
embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION(
query, prefix=prefix, user=user
),
k=request.app.state.config.TOP_K,
reranking_function=request.app.state.rf,
k_reranker=request.app.state.config.TOP_K_RERANKER,
r=request.app.state.config.RELEVANCE_THRESHOLD,
hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
full_context=request.app.state.config.RAG_FULL_CONTEXT,
@@ -590,31 +621,42 @@ def apply_params_to_form_data(form_data, model):
if "keep_alive" in params:
form_data["keep_alive"] = params["keep_alive"]
else:
if "seed" in params:
if "seed" in params and params["seed"] is not None:
form_data["seed"] = params["seed"]
if "stop" in params:
if "stop" in params and params["stop"] is not None:
form_data["stop"] = params["stop"]
if "temperature" in params:
if "temperature" in params and params["temperature"] is not None:
form_data["temperature"] = params["temperature"]
if "max_tokens" in params:
if "max_tokens" in params and params["max_tokens"] is not None:
form_data["max_tokens"] = params["max_tokens"]
if "top_p" in params:
if "top_p" in params and params["top_p"] is not None:
form_data["top_p"] = params["top_p"]
if "frequency_penalty" in params:
if "frequency_penalty" in params and params["frequency_penalty"] is not None:
form_data["frequency_penalty"] = params["frequency_penalty"]
if "reasoning_effort" in params:
if "presence_penalty" in params and params["presence_penalty"] is not None:
form_data["presence_penalty"] = params["presence_penalty"]
if "reasoning_effort" in params and params["reasoning_effort"] is not None:
form_data["reasoning_effort"] = params["reasoning_effort"]
if "logit_bias" in params and params["logit_bias"] is not None:
try:
form_data["logit_bias"] = json.loads(
convert_logit_bias_input_to_json(params["logit_bias"])
)
except Exception as e:
print(f"Error parsing logit_bias: {e}")
return form_data
async def process_chat_payload(request, form_data, metadata, user, model):
async def process_chat_payload(request, form_data, user, metadata, model):
form_data = apply_params_to_form_data(form_data, model)
log.debug(f"form_data: {form_data}")
@@ -707,9 +749,14 @@ async def process_chat_payload(request, form_data, metadata, user, model):
raise e
try:
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model)
]
form_data, flags = await process_filter_functions(
request=request,
filter_ids=get_sorted_filter_ids(model),
filter_functions=filter_functions,
filter_type="inlet",
form_data=form_data,
extra_params=extra_params,
@@ -753,12 +800,18 @@ async def process_chat_payload(request, form_data, metadata, user, model):
}
form_data["metadata"] = metadata
# Server side tools
tool_ids = metadata.get("tool_ids", None)
# Client side tools
tool_servers = metadata.get("tool_servers", None)
log.debug(f"{tool_ids=}")
log.debug(f"{tool_servers=}")
tools_dict = {}
if tool_ids:
# If tool_ids field is present, then get the tools
tools = get_tools(
tools_dict = get_tools(
request,
tool_ids,
user,
@@ -769,20 +822,31 @@ async def process_chat_payload(request, form_data, metadata, user, model):
"__files__": metadata.get("files", []),
},
)
log.info(f"{tools=}")
if tool_servers:
for tool_server in tool_servers:
tool_specs = tool_server.pop("specs", [])
for tool in tool_specs:
tools_dict[tool["name"]] = {
"spec": tool,
"direct": True,
"server": tool_server,
}
if tools_dict:
if metadata.get("function_calling") == "native":
# If the function calling is native, then call the tools function calling handler
metadata["tools"] = tools
metadata["tools"] = tools_dict
form_data["tools"] = [
{"type": "function", "function": tool.get("spec", {})}
for tool in tools.values()
for tool in tools_dict.values()
]
else:
# If the function calling is not native, then call the tools function calling handler
try:
form_data, flags = await chat_completion_tools_handler(
request, form_data, user, models, tools
request, form_data, extra_params, user, models, tools_dict
)
sources.extend(flags.get("sources", []))
@@ -798,10 +862,20 @@ async def process_chat_payload(request, form_data, metadata, user, model):
# If context is not empty, insert it into the messages
if len(sources) > 0:
context_string = ""
for source_idx, source in enumerate(sources):
citation_idx = {}
for source in sources:
if "document" in source:
for doc_idx, doc_context in enumerate(source["document"]):
context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
for doc_context, doc_meta in zip(
source["document"], source["metadata"]
):
citation_id = (
doc_meta.get("source", None)
or source.get("source", {}).get("id", None)
or "N/A"
)
if citation_id not in citation_idx:
citation_idx[citation_id] = len(citation_idx) + 1
context_string += f'<source id="{citation_idx[citation_id]}">{doc_context}</source>\n'
context_string = context_string.strip()
prompt = get_last_user_message(form_data["messages"])
@@ -834,7 +908,12 @@ async def process_chat_payload(request, form_data, metadata, user, model):
)
# If there are citations, add them to the data_items
sources = [source for source in sources if source.get("source", {}).get("name", "")]
sources = [
source
for source in sources
if source.get("source", {}).get("name", "")
or source.get("source", {}).get("id", "")
]
if len(sources) > 0:
events.append({"sources": sources})
@@ -856,7 +935,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
async def process_chat_response(
request, response, form_data, user, events, metadata, tasks
request, response, form_data, user, metadata, model, events, tasks
):
async def background_tasks_handler():
message_map = Chats.get_messages_by_chat_id(metadata["chat_id"])
@@ -865,6 +944,20 @@ async def process_chat_response(
if message:
messages = get_message_list(message_map, message.get("id"))
# Remove reasoning details and files from the messages.
# as get_message_list creates a new list, it does not affect
# the original messages outside of this handler
for message in messages:
message["content"] = re.sub(
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
"",
message["content"],
flags=re.S,
).strip()
if message.get("files"):
message["files"] = []
if tasks and messages:
if TASKS.TITLE_GENERATION in tasks:
if tasks[TASKS.TITLE_GENERATION]:
@@ -978,6 +1071,16 @@ async def process_chat_response(
# Non-streaming response
if not isinstance(response, StreamingResponse):
if event_emitter:
if "error" in response:
error = response["error"].get("detail", response["error"])
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"error": {"content": error},
},
)
if "selected_model_id" in response:
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
@@ -987,7 +1090,8 @@ async def process_chat_response(
},
)
if response.get("choices", [])[0].get("message", {}).get("content"):
choices = response.get("choices", [])
if choices and choices[0].get("message", {}).get("content"):
content = response["choices"][0]["message"]["content"]
if content:
@@ -1022,7 +1126,7 @@ async def process_chat_response(
)
# Send a webhook notification if the user is not active
if get_active_status_by_user_id(user.id) is None:
if not get_active_status_by_user_id(user.id):
webhook_url = Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
post_webhook(
@@ -1061,9 +1165,12 @@ async def process_chat_response(
},
"__metadata__": metadata,
"__request__": request,
"__model__": metadata.get("model"),
"__model__": model,
}
filter_ids = get_sorted_filter_ids(form_data.get("model"))
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model)
]
# Streaming response
if event_emitter and event_caller:
@@ -1103,36 +1210,53 @@ async def process_chat_response(
elif block["type"] == "tool_calls":
attributes = block.get("attributes", {})
block_content = block.get("content", [])
tool_calls = block.get("content", [])
results = block.get("results", [])
if results:
result_display_content = ""
tool_calls_display_content = ""
for tool_call in tool_calls:
for result in results:
tool_call_id = result.get("tool_call_id", "")
tool_name = ""
tool_call_id = tool_call.get("id", "")
tool_name = tool_call.get("function", {}).get(
"name", ""
)
tool_arguments = tool_call.get("function", {}).get(
"arguments", ""
)
for tool_call in block_content:
if tool_call.get("id", "") == tool_call_id:
tool_name = tool_call.get("function", {}).get(
"name", ""
)
tool_result = None
tool_result_files = None
for result in results:
if tool_call_id == result.get("tool_call_id", ""):
tool_result = result.get("content", None)
tool_result_files = result.get("files", None)
break
result_display_content = f"{result_display_content}\n> {tool_name}: {result.get('content', '')}"
if tool_result:
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}">\n<summary>Tool Executed</summary>\n</details>\n'
else:
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>'
if not raw:
content = f'{content}\n<details type="tool_calls" done="true" content="{html.escape(json.dumps(block_content))}" results="{html.escape(json.dumps(results))}">\n<summary>Tool Executed</summary>\n{result_display_content}\n</details>\n'
content = f"{content}\n{tool_calls_display_content}\n\n"
else:
tool_calls_display_content = ""
for tool_call in block_content:
tool_calls_display_content = f"{tool_calls_display_content}\n> Executing {tool_call.get('function', {}).get('name', '')}"
for tool_call in tool_calls:
tool_call_id = tool_call.get("id", "")
tool_name = tool_call.get("function", {}).get(
"name", ""
)
tool_arguments = tool_call.get("function", {}).get(
"arguments", ""
)
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>'
if not raw:
content = f'{content}\n<details type="tool_calls" done="false" content="{html.escape(json.dumps(block_content))}">\n<summary>Tool Executing...</summary>\n{tool_calls_display_content}\n</details>\n'
content = f"{content}\n{tool_calls_display_content}\n\n"
elif block["type"] == "reasoning":
reasoning_display_content = "\n".join(
@@ -1290,6 +1414,9 @@ async def process_chat_response(
if after_tag:
content_blocks[-1]["content"] = after_tag
tag_content_handler(
content_type, tags, after_tag, content_blocks
)
break
elif content_blocks[-1]["type"] == content_type:
@@ -1470,13 +1597,16 @@ async def process_chat_response(
data, _ = await process_filter_functions(
request=request,
filter_ids=filter_ids,
filter_functions=filter_functions,
filter_type="stream",
form_data=data,
extra_params=extra_params,
)
if data:
if "event" in data:
await event_emitter(data.get("event", {}))
if "selected_model_id" in data:
model_id = data["selected_model_id"]
Chats.upsert_message_to_chat_by_id_and_message_id(
@@ -1489,6 +1619,16 @@ async def process_chat_response(
else:
choices = data.get("choices", [])
if not choices:
error = data.get("error", {})
if error:
await event_emitter(
{
"type": "chat:completion",
"data": {
"error": error,
},
}
)
usage = data.get("usage", {})
if usage:
await event_emitter(
@@ -1511,14 +1651,36 @@ async def process_chat_response(
)
if tool_call_index is not None:
if (
len(response_tool_calls)
<= tool_call_index
):
# Check if the tool call already exists
current_response_tool_call = None
for (
response_tool_call
) in response_tool_calls:
if (
response_tool_call.get("index")
== tool_call_index
):
current_response_tool_call = (
response_tool_call
)
break
if current_response_tool_call is None:
# Add the new tool call
delta_tool_call.setdefault(
"function", {}
)
delta_tool_call[
"function"
].setdefault("name", "")
delta_tool_call[
"function"
].setdefault("arguments", "")
response_tool_calls.append(
delta_tool_call
)
else:
# Update the existing tool call
delta_name = delta_tool_call.get(
"function", {}
).get("name")
@@ -1529,24 +1691,74 @@ async def process_chat_response(
)
if delta_name:
response_tool_calls[
tool_call_index
]["function"][
"name"
] += delta_name
current_response_tool_call[
"function"
]["name"] += delta_name
if delta_arguments:
response_tool_calls[
tool_call_index
]["function"][
current_response_tool_call[
"function"
][
"arguments"
] += delta_arguments
value = delta.get("content")
if value:
content = f"{content}{value}"
reasoning_content = delta.get(
"reasoning_content"
) or delta.get("reasoning")
if reasoning_content:
if (
not content_blocks
or content_blocks[-1]["type"] != "reasoning"
):
reasoning_block = {
"type": "reasoning",
"start_tag": "think",
"end_tag": "/think",
"attributes": {
"type": "reasoning_content"
},
"content": "",
"started_at": time.time(),
}
content_blocks.append(reasoning_block)
else:
reasoning_block = content_blocks[-1]
reasoning_block["content"] += reasoning_content
data = {
"content": serialize_content_blocks(
content_blocks
)
}
if value:
if (
content_blocks
and content_blocks[-1]["type"]
== "reasoning"
and content_blocks[-1]
.get("attributes", {})
.get("type")
== "reasoning_content"
):
reasoning_block = content_blocks[-1]
reasoning_block["ended_at"] = time.time()
reasoning_block["duration"] = int(
reasoning_block["ended_at"]
- reasoning_block["started_at"]
)
content_blocks.append(
{
"type": "text",
"content": "",
}
)
content = f"{content}{value}"
if not content_blocks:
content_blocks.append(
{
@@ -1650,7 +1862,7 @@ async def process_chat_response(
await stream_body_handler(response)
MAX_TOOL_CALL_RETRIES = 5
MAX_TOOL_CALL_RETRIES = 10
tool_call_retries = 0
while len(tool_calls) > 0 and tool_call_retries < MAX_TOOL_CALL_RETRIES:
@@ -1689,6 +1901,15 @@ async def process_chat_response(
)
except Exception as e:
log.debug(e)
# Fallback to JSON parsing
try:
tool_function_params = json.loads(
tool_call.get("function", {}).get("arguments", "{}")
)
except Exception as e:
log.debug(
f"Error parsing tool call arguments: {tool_call.get('function', {}).get('arguments', '{}')}"
)
tool_result = None
@@ -1697,25 +1918,65 @@ async def process_chat_response(
spec = tool.get("spec", {})
try:
required_params = spec.get("parameters", {}).get(
"required", []
allowed_params = (
spec.get("parameters", {})
.get("properties", {})
.keys()
)
tool_function = tool["callable"]
tool_function_params = {
k: v
for k, v in tool_function_params.items()
if k in required_params
if k in allowed_params
}
tool_result = await tool_function(
**tool_function_params
)
if tool.get("direct", False):
tool_result = await event_caller(
{
"type": "execute:tool",
"data": {
"id": str(uuid4()),
"name": tool_name,
"params": tool_function_params,
"server": tool.get("server", {}),
"session_id": metadata.get(
"session_id", None
),
},
}
)
else:
tool_function = tool["callable"]
tool_result = await tool_function(
**tool_function_params
)
except Exception as e:
tool_result = str(e)
tool_result_files = []
if isinstance(tool_result, list):
for item in tool_result:
# check if string
if isinstance(item, str) and item.startswith("data:"):
tool_result_files.append(item)
tool_result.remove(item)
if isinstance(tool_result, dict) or isinstance(
tool_result, list
):
tool_result = json.dumps(tool_result, indent=2)
results.append(
{
"tool_call_id": tool_call_id,
"content": tool_result,
**(
{"files": tool_result_files}
if tool_result_files
else {}
),
}
)
@@ -1914,11 +2175,6 @@ async def process_chat_response(
}
)
log.info(f"content_blocks={content_blocks}")
log.info(
f"serialize_content_blocks={serialize_content_blocks(content_blocks)}"
)
try:
res = await generate_chat_completion(
request,
@@ -1964,7 +2220,7 @@ async def process_chat_response(
)
# Send a webhook notification if the user is not active
if get_active_status_by_user_id(user.id) is None:
if not get_active_status_by_user_id(user.id):
webhook_url = Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
post_webhook(
@@ -2005,7 +2261,9 @@ async def process_chat_response(
await response.background()
# background_tasks.add_task(post_response_handler, response, events)
task_id, _ = create_task(post_response_handler(response, events))
task_id, _ = create_task(
post_response_handler(response, events), id=metadata["chat_id"]
)
return {"status": True, "task_id": task_id}
else:
@@ -2017,7 +2275,7 @@ async def process_chat_response(
for event in events:
event, _ = await process_filter_functions(
request=request,
filter_ids=filter_ids,
filter_functions=filter_functions,
filter_type="stream",
form_data=event,
extra_params=extra_params,
@@ -2029,7 +2287,7 @@ async def process_chat_response(
async for data in original_generator:
data, _ = await process_filter_functions(
request=request,
filter_ids=filter_ids,
filter_functions=filter_functions,
filter_type="stream",
form_data=data,
extra_params=extra_params,
+13
View File
@@ -6,6 +6,7 @@ import logging
from datetime import timedelta
from pathlib import Path
from typing import Callable, Optional
import json
import collections.abc
@@ -450,3 +451,15 @@ def parse_ollama_modelfile(model_text):
data["params"]["messages"] = messages
return data
def convert_logit_bias_input_to_json(user_input):
logit_bias_pairs = user_input.split(",")
logit_bias_json = {}
for pair in logit_bias_pairs:
token, bias = pair.split(":")
token = str(token.strip())
bias = int(bias.strip())
bias = 100 if bias > 100 else -100 if bias < -100 else bias
logit_bias_json[token] = bias
return json.dumps(logit_bias_json)
+8 -3
View File
@@ -49,6 +49,7 @@ async def get_all_base_models(request: Request, user: UserModel = None):
"created": int(time.time()),
"owned_by": "ollama",
"ollama": model,
"tags": model.get("tags", []),
}
for model in ollama_models["models"]
]
@@ -113,9 +114,12 @@ async def get_all_models(request, user: UserModel = None):
for custom_model in custom_models:
if custom_model.base_model_id is None:
for model in models:
if (
custom_model.id == model["id"]
or custom_model.id == model["id"].split(":")[0]
if custom_model.id == model["id"] or (
model.get("owned_by") == "ollama"
and custom_model.id
== model["id"].split(":")[
0
] # Ollama may return model ids in different formats (e.g., 'llama3' vs. 'llama3:7b')
):
if custom_model.is_active:
model["name"] = custom_model.name
@@ -199,6 +203,7 @@ async def get_all_models(request, user: UserModel = None):
else:
function_module, _, _ = load_function_module_by_id(function_id)
request.app.state.FUNCTIONS[function_id] = function_module
return function_module
for model in models:
action_ids = [
+148 -43
View File
@@ -3,6 +3,7 @@ import logging
import mimetypes
import sys
import uuid
import json
import aiohttp
from authlib.integrations.starlette_client import OAuth
@@ -15,7 +16,7 @@ from starlette.responses import RedirectResponse
from open_webui.models.auths import Auths
from open_webui.models.users import Users
from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm
from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm, GroupForm
from open_webui.config import (
DEFAULT_USER_ROLE,
ENABLE_OAUTH_SIGNUP,
@@ -23,6 +24,8 @@ from open_webui.config import (
OAUTH_PROVIDERS,
ENABLE_OAUTH_ROLE_MANAGEMENT,
ENABLE_OAUTH_GROUP_MANAGEMENT,
ENABLE_OAUTH_GROUP_CREATION,
OAUTH_BLOCKED_GROUPS,
OAUTH_ROLES_CLAIM,
OAUTH_GROUPS_CLAIM,
OAUTH_EMAIL_CLAIM,
@@ -31,6 +34,7 @@ from open_webui.config import (
OAUTH_ALLOWED_ROLES,
OAUTH_ADMIN_ROLES,
OAUTH_ALLOWED_DOMAINS,
OAUTH_UPDATE_PICTURE_ON_LOGIN,
WEBHOOK_URL,
JWT_EXPIRES_IN,
AppConfig,
@@ -57,6 +61,8 @@ auth_manager_config.ENABLE_OAUTH_SIGNUP = ENABLE_OAUTH_SIGNUP
auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL = OAUTH_MERGE_ACCOUNTS_BY_EMAIL
auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT = ENABLE_OAUTH_GROUP_MANAGEMENT
auth_manager_config.ENABLE_OAUTH_GROUP_CREATION = ENABLE_OAUTH_GROUP_CREATION
auth_manager_config.OAUTH_BLOCKED_GROUPS = OAUTH_BLOCKED_GROUPS
auth_manager_config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
auth_manager_config.OAUTH_GROUPS_CLAIM = OAUTH_GROUPS_CLAIM
auth_manager_config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
@@ -67,6 +73,7 @@ auth_manager_config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
auth_manager_config.OAUTH_ALLOWED_DOMAINS = OAUTH_ALLOWED_DOMAINS
auth_manager_config.WEBHOOK_URL = WEBHOOK_URL
auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
class OAuthManager:
@@ -94,7 +101,7 @@ class OAuthManager:
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
oauth_roles = None
oauth_roles = []
# Default/fallback role if no matching roles are found
role = auth_manager_config.DEFAULT_USER_ROLE
@@ -104,7 +111,7 @@ class OAuthManager:
nested_claims = oauth_claim.split(".")
for nested_claim in nested_claims:
claim_data = claim_data.get(nested_claim, {})
oauth_roles = claim_data if isinstance(claim_data, list) else None
oauth_roles = claim_data if isinstance(claim_data, list) else []
log.debug(f"Oauth Roles claim: {oauth_claim}")
log.debug(f"User roles from oauth: {oauth_roles}")
@@ -140,17 +147,75 @@ class OAuthManager:
log.debug("Running OAUTH Group management")
oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
try:
blocked_groups = json.loads(auth_manager_config.OAUTH_BLOCKED_GROUPS)
except Exception as e:
log.exception(f"Error loading OAUTH_BLOCKED_GROUPS: {e}")
blocked_groups = []
user_oauth_groups = []
# Nested claim search for groups claim
if oauth_claim:
claim_data = user_data
nested_claims = oauth_claim.split(".")
for nested_claim in nested_claims:
claim_data = claim_data.get(nested_claim, {})
user_oauth_groups = claim_data if isinstance(claim_data, list) else []
if isinstance(claim_data, list):
user_oauth_groups = claim_data
elif isinstance(claim_data, str):
user_oauth_groups = [claim_data]
else:
user_oauth_groups = []
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
all_available_groups: list[GroupModel] = Groups.get_groups()
# Create groups if they don't exist and creation is enabled
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
log.debug("Checking for missing groups to create...")
all_group_names = {g.name for g in all_available_groups}
groups_created = False
# Determine creator ID: Prefer admin, fallback to current user if no admin exists
admin_user = Users.get_super_admin_user()
creator_id = admin_user.id if admin_user else user.id
log.debug(f"Using creator ID {creator_id} for potential group creation.")
for group_name in user_oauth_groups:
if group_name not in all_group_names:
log.info(
f"Group '{group_name}' not found via OAuth claim. Creating group..."
)
try:
new_group_form = GroupForm(
name=group_name,
description=f"Group '{group_name}' created automatically via OAuth.",
permissions=default_permissions, # Use default permissions from function args
user_ids=[], # Start with no users, user will be added later by subsequent logic
)
# Use determined creator ID (admin or fallback to current user)
created_group = Groups.insert_new_group(
creator_id, new_group_form
)
if created_group:
log.info(
f"Successfully created group '{group_name}' with ID {created_group.id} using creator ID {creator_id}"
)
groups_created = True
# Add to local set to prevent duplicate creation attempts in this run
all_group_names.add(group_name)
else:
log.error(
f"Failed to create group '{group_name}' via OAuth."
)
except Exception as e:
log.error(f"Error creating group '{group_name}' via OAuth: {e}")
# Refresh the list of all available groups if any were created
if groups_created:
all_available_groups = Groups.get_groups()
log.debug("Refreshed list of all available groups after creation.")
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]}")
@@ -160,7 +225,11 @@ class OAuthManager:
# 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:
if (
user_oauth_groups
and group_model.name not in user_oauth_groups
and group_model.name not in blocked_groups
):
# Remove group from user
log.debug(
f"Removing user from group {group_model.name} as it is no longer in their oauth groups"
@@ -186,8 +255,11 @@ class OAuthManager:
# Add user to new groups
for group_model in all_available_groups:
if group_model.name in user_oauth_groups and not any(
gm.name == group_model.name for gm in user_current_groups
if (
user_oauth_groups
and group_model.name in user_oauth_groups
and not any(gm.name == group_model.name for gm in user_current_groups)
and group_model.name not in blocked_groups
):
# Add user to group
log.debug(
@@ -212,6 +284,49 @@ class OAuthManager:
id=group_model.id, form_data=update_form, overwrite=False
)
async def _process_picture_url(
self, picture_url: str, access_token: str = None
) -> str:
"""Process a picture URL and return a base64 encoded data URL.
Args:
picture_url: The URL of the picture to process
access_token: Optional OAuth access token for authenticated requests
Returns:
A data URL containing the base64 encoded picture, or "/user.png" if processing fails
"""
if not picture_url:
return "/user.png"
try:
get_kwargs = {}
if access_token:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with session.get(picture_url, **get_kwargs) as resp:
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:
guessed_mime_type = "image/jpeg"
return (
f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
)
else:
log.warning(
f"Failed to fetch profile picture from {picture_url}"
)
return "/user.png"
except Exception as e:
log.error(f"Error processing profile picture '{picture_url}': {e}")
return "/user.png"
async def handle_login(self, request, provider):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
@@ -234,7 +349,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 or "email" not in user_data:
if not user_data or auth_manager_config.OAUTH_EMAIL_CLAIM 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}")
@@ -254,7 +369,7 @@ class OAuthManager:
try:
access_token = token.get("access_token")
headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
"https://api.github.com/user/emails", headers=headers
) as resp:
@@ -312,6 +427,22 @@ class OAuthManager:
if user.role != determined_role:
Users.update_user_role_by_id(user.id, determined_role)
# Update profile picture if enabled and different from current
if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
if picture_claim:
new_picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
processed_picture_url = await self._process_picture_url(
new_picture_url, token.get("access_token")
)
if processed_picture_url != user.profile_image_url:
Users.update_user_profile_image_url_by_id(
user.id, processed_picture_url
)
log.debug(f"Updated profile picture for user {user.email}")
if not user:
user_count = Users.get_num_users()
@@ -323,40 +454,14 @@ class OAuthManager:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
if picture_url:
# Download the profile image into a base64 string
try:
access_token = token.get("access_token")
get_kwargs = {}
if access_token:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with session.get(picture_url, **get_kwargs) as resp:
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}"
)
picture_url = "/user.png"
if not picture_url:
if picture_claim:
picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
picture_url = await self._process_picture_url(
picture_url, token.get("access_token")
)
else:
picture_url = "/user.png"
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
+26
View File
@@ -59,9 +59,12 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
"top_p": float,
"max_tokens": int,
"frequency_penalty": float,
"presence_penalty": float,
"reasoning_effort": str,
"seed": lambda x: x,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
"logit_bias": lambda x: x,
"response_format": dict,
}
return apply_model_params_to_body(params, form_data, mappings)
@@ -109,6 +112,15 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
"num_thread": int,
}
# Extract keep_alive from options if it exists
if "options" in form_data and "keep_alive" in form_data["options"]:
form_data["keep_alive"] = form_data["options"]["keep_alive"]
del form_data["options"]["keep_alive"]
if "options" in form_data and "format" in form_data["options"]:
form_data["format"] = form_data["options"]["format"]
del form_data["options"]["format"]
return apply_model_params_to_body(params, form_data, mappings)
@@ -230,6 +242,11 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
"system"
] # To prevent Ollama warning of invalid option provided
# Extract keep_alive from options if it exists
if "keep_alive" in ollama_options:
ollama_payload["keep_alive"] = ollama_options["keep_alive"]
del ollama_options["keep_alive"]
# If there is the "stop" parameter in the openai_payload, remap it to the ollama_payload.options
if "stop" in openai_payload:
ollama_options = ollama_payload.get("options", {})
@@ -239,4 +256,13 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
if "metadata" in openai_payload:
ollama_payload["metadata"] = openai_payload["metadata"]
if "response_format" in openai_payload:
response_format = openai_payload["response_format"]
format_type = response_format.get("type", None)
schema = response_format.get(format_type, None)
if schema:
format = schema.get("schema", None)
ollama_payload["format"] = format
return ollama_payload
+1 -1
View File
@@ -110,7 +110,7 @@ class PDFGenerator:
# When running using `pip install -e .` the static directory is in the site packages.
# This path only works if `open-webui serve` is run from the root of this project.
if not FONTS_DIR.exists():
FONTS_DIR = Path("./backend/static/fonts")
FONTS_DIR = Path(".") / "backend" / "static" / "fonts"
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
+47 -13
View File
@@ -7,7 +7,7 @@ import types
import tempfile
import logging
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import SRC_LOG_LEVELS, PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS
from open_webui.models.functions import Functions
from open_webui.models.tools import Tools
@@ -68,23 +68,23 @@ def replace_imports(content):
return content
def load_tools_module_by_id(toolkit_id, content=None):
def load_tool_module_by_id(tool_id, content=None):
if content is None:
tool = Tools.get_tool_by_id(toolkit_id)
tool = Tools.get_tool_by_id(tool_id)
if not tool:
raise Exception(f"Toolkit not found: {toolkit_id}")
raise Exception(f"Toolkit not found: {tool_id}")
content = tool.content
content = replace_imports(content)
Tools.update_tool_by_id(toolkit_id, {"content": content})
Tools.update_tool_by_id(tool_id, {"content": content})
else:
frontmatter = extract_frontmatter(content)
# Install required packages found within the frontmatter
install_frontmatter_requirements(frontmatter.get("requirements", ""))
module_name = f"tool_{toolkit_id}"
module_name = f"tool_{tool_id}"
module = types.ModuleType(module_name)
sys.modules[module_name] = module
@@ -108,7 +108,7 @@ def load_tools_module_by_id(toolkit_id, content=None):
else:
raise Exception("No Tools class found in the module")
except Exception as e:
log.error(f"Error loading module: {toolkit_id}: {e}")
log.error(f"Error loading module: {tool_id}: {e}")
del sys.modules[module_name] # Clean up
raise e
finally:
@@ -157,7 +157,8 @@ def load_function_module_by_id(function_id, content=None):
raise Exception("No Function class found in the module")
except Exception as e:
log.error(f"Error loading module: {function_id}: {e}")
del sys.modules[module_name] # Cleanup by removing the module in case of error
# Cleanup by removing the module in case of error
del sys.modules[module_name]
Functions.update_function_by_id(function_id, {"is_active": False})
raise e
@@ -165,16 +166,49 @@ def load_function_module_by_id(function_id, content=None):
os.unlink(temp_file.name)
def install_frontmatter_requirements(requirements):
def install_frontmatter_requirements(requirements: str):
if requirements:
try:
req_list = [req.strip() for req in requirements.split(",")]
for req in req_list:
log.info(f"Installing requirement: {req}")
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
log.info(f"Installing requirements: {' '.join(req_list)}")
subprocess.check_call(
[sys.executable, "-m", "pip", "install"]
+ PIP_OPTIONS
+ req_list
+ PIP_PACKAGE_INDEX_OPTIONS
)
except Exception as e:
log.error(f"Error installing package: {req}")
log.error(f"Error installing packages: {' '.join(req_list)}")
raise e
else:
log.info("No requirements found in frontmatter.")
def install_tool_and_function_dependencies():
"""
Install all dependencies for all admin tools and active functions.
By first collecting all dependencies from the frontmatter of each tool and function,
and then installing them using pip. Duplicates or similar version specifications are
handled by pip as much as possible.
"""
function_list = Functions.get_functions(active_only=True)
tool_list = Tools.get_tools()
all_dependencies = ""
try:
for function in function_list:
frontmatter = extract_frontmatter(replace_imports(function.content))
if dependencies := frontmatter.get("requirements"):
all_dependencies += f"{dependencies}, "
for tool in tool_list:
# Only install requirements for admin tools
if tool.user.role == "admin":
frontmatter = extract_frontmatter(replace_imports(tool.content))
if dependencies := frontmatter.get("requirements"):
all_dependencies += f"{dependencies}, "
install_frontmatter_requirements(all_dependencies.strip(", "))
except Exception as e:
log.error(f"Error installing requirements: {e}")
+58
View File
@@ -0,0 +1,58 @@
import socketio
import redis
from redis import asyncio as aioredis
from urllib.parse import urlparse
def parse_redis_service_url(redis_url):
parsed_url = urlparse(redis_url)
if parsed_url.scheme != "redis":
raise ValueError("Invalid Redis URL scheme. Must be 'redis'.")
return {
"username": parsed_url.username or None,
"password": parsed_url.password or None,
"service": parsed_url.hostname or "mymaster",
"port": parsed_url.port or 6379,
"db": int(parsed_url.path.lstrip("/") or 0),
}
def get_redis_connection(redis_url, redis_sentinels, decode_responses=True):
if redis_sentinels:
redis_config = parse_redis_service_url(redis_url)
sentinel = redis.sentinel.Sentinel(
redis_sentinels,
port=redis_config["port"],
db=redis_config["db"],
username=redis_config["username"],
password=redis_config["password"],
decode_responses=decode_responses,
)
# Get a master connection from Sentinel
return sentinel.master_for(redis_config["service"])
else:
# Standard Redis connection
return redis.Redis.from_url(redis_url, decode_responses=decode_responses)
def get_sentinels_from_env(sentinel_hosts_env, sentinel_port_env):
if sentinel_hosts_env:
sentinel_hosts = sentinel_hosts_env.split(",")
sentinel_port = int(sentinel_port_env)
return [(host, sentinel_port) for host in sentinel_hosts]
return []
def get_sentinel_url_from_env(redis_url, sentinel_hosts_env, sentinel_port_env):
redis_config = parse_redis_service_url(redis_url)
username = redis_config["username"] or ""
password = redis_config["password"] or ""
auth_part = ""
if username or password:
auth_part = f"{username}:{password}@"
hosts_part = ",".join(
f"{host}:{sentinel_port_env}" for host in sentinel_hosts_env.split(",")
)
return f"redis+sentinel://{auth_part}{hosts_part}/{redis_config['db']}/{redis_config['service']}"
+3 -1
View File
@@ -104,7 +104,7 @@ def replace_prompt_variable(template: str, prompt: str) -> str:
def replace_messages_variable(
template: str, messages: Optional[list[str]] = None
template: str, messages: Optional[list[dict]] = None
) -> str:
def replacement_function(match):
full_match = match.group(0)
@@ -152,6 +152,8 @@ def rag_template(template: str, context: str, query: str):
if template.strip() == "":
template = DEFAULT_RAG_TEMPLATE
template = prompt_template(template)
if "[context]" not in template and "{{CONTEXT}}" not in template:
log.debug(
"WARNING: The RAG template does not contain the '[context]' or '{{CONTEXT}}' placeholder."

Some files were not shown because too many files have changed in this diff Show More