Compare commits

..

1420 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 0a26c41c7b Merge pull request #7399 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.4.6
2024-11-26 20:24:33 -08:00
Timothy Jaeryang Baek da6535eeb4 refac: wording 2024-11-26 20:19:46 -08:00
Timothy Jaeryang Baek 2db35d5969 chore: format 2024-11-26 20:14:48 -08:00
Timothy Jaeryang Baek 582ce23bb5 doc: changelog 2024-11-26 20:12:01 -08:00
Timothy Jaeryang Baek f42cc90a00 doc: changelogs 2024-11-26 20:03:10 -08:00
Timothy Jaeryang Baek cbc7801b0e chore: format 2024-11-26 20:03:06 -08:00
Timothy Jaeryang Baek 22b5feb747 Merge pull request #7401 from walker-chen2024/main
bugfix - {{CURRENT_WEEKDAY}} is not working
2024-11-26 19:54:27 -08:00
Walker.Chen 4b536b5283 bugfix - {{CURRENT_WEEKDAY}} is not working
Signed-off-by: Walker.Chen <walker_chen@moremote.com>  https://uniwillai.net
2024-11-27 08:53:45 +08:00
Timothy Jaeryang Baek ede29e98b7 refac 2024-11-26 15:30:35 -08:00
Timothy Jaeryang Baek 789e1db260 refac: textarea 2024-11-26 14:14:26 -08:00
Timothy Jaeryang Baek 4383306770 refac 2024-11-26 14:00:49 -08:00
Timothy Jaeryang Baek 13796fe3b3 fix: legacy query generation support 2024-11-26 13:44:19 -08:00
Timothy Jaeryang Baek 63402c48a8 refac: query gen prompt 2024-11-26 10:23:29 -08:00
Timothy Jaeryang Baek 44efd4d372 refac 2024-11-26 10:17:17 -08:00
Timothy Jaeryang Baek a18292da84 Merge pull request #7381 from panda44312/dev
i18n - Update Simplified Chinese translation
2024-11-26 08:59:40 -08:00
Timothy Jaeryang Baek adba11ebeb Merge pull request #7382 from juananpe/dev
i18n: Add Basque (eu-ES, Euskara) translation
2024-11-26 08:59:30 -08:00
Timothy Jaeryang Baek 442f99e075 refac 2024-11-26 08:55:34 -08:00
Timothy Jaeryang Baek dc7221816f fix: models configure 2024-11-26 08:55:06 -08:00
Juanan Pereira bddc293d82 Add Basque (eu-ES, Euskara) translation 2024-11-26 14:18:59 +01:00
panda44312 3a2247b7a0 Update translation.json 更新翻译 2024-11-26 13:48:28 +01:00
Timothy Jaeryang Baek 4831c9e57e Merge pull request #7307 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.4.5
2024-11-26 01:55:13 -08:00
Timothy Jaeryang Baek e3236622f3 chore: format 2024-11-26 01:53:06 -08:00
Timothy Jaeryang Baek 83f1fa7bb9 Merge pull request #7373 from open-webui/main
dev
2024-11-26 01:52:45 -08:00
Timothy Jaeryang Baek ba427bee3f Merge pull request #7369 from PieterBecking/fix/i18n-typo-groepen
fix(i18n): Correct capitalization typo in Dutch localization
2024-11-26 01:52:24 -08:00
Timothy Jaeryang Baek b9458817b1 doc: changelogs 2024-11-26 01:51:04 -08:00
Timothy Jaeryang Baek 38eb6abbfc chore: version bump 2024-11-26 01:43:28 -08:00
Timothy Jaeryang Baek b173f86690 fix: escape source id 2024-11-26 01:39:12 -08:00
Timothy Jaeryang Baek 9b8f9c689b refac 2024-11-26 01:22:09 -08:00
Timothy Jaeryang Baek e40212a662 refac 2024-11-26 01:14:32 -08:00
Timothy Jaeryang Baek a37cad2ecf chore: format 2024-11-26 01:05:50 -08:00
Timothy Jaeryang Baek 3b9e21ecf9 refac 2024-11-26 00:58:40 -08:00
Timothy Jaeryang Baek 5fac25a002 enh: reintroduce model order/default models 2024-11-26 00:55:58 -08:00
Pieter Becking 2b4e8f6cea fix(i18n): Correct capitalization typo in Dutch localization (GRoepen -> Groepen) 2024-11-26 09:42:13 +01:00
Timothy Jaeryang Baek 29fac5ecca refac: admin models settings 2024-11-25 22:57:54 -08:00
Timothy Jaeryang Baek f9e24968e3 fix: input issue 2024-11-25 22:43:34 -08:00
Timothy Jaeryang Baek c4f82309dc fix: min_p save issue 2024-11-25 16:11:49 -08:00
Timothy Jaeryang Baek da4676de2e Merge pull request #7326 from bnodnarb/fix/ollama-authentication
fix: Include Authorization header in /api/pull and /api/chat requests
2024-11-25 15:38:43 -08:00
Timothy Jaeryang Baek d870386d7d Update CODE_OF_CONDUCT.md 2024-11-24 23:48:02 -08:00
bnodnarb 8dc73e8744 Fix: Add authorization header with bearer token for remote Ollama server endpoints 2024-11-24 20:29:54 -10:00
Timothy Jaeryang Baek 840437e58f refac: o1 title generation issue 2024-11-24 19:07:51 -08:00
Timothy Jaeryang Baek bd28e1ed7d refac: rag prompt template 2024-11-24 18:49:56 -08:00
Timothy Jaeryang Baek 50c3be2136 refac 2024-11-24 18:11:48 -08:00
Timothy Jaeryang Baek 907cf61da7 refac: query generation 2024-11-24 18:03:58 -08:00
Timothy Jaeryang Baek a3a205abd1 fix: function list 2024-11-24 15:45:23 -08:00
Timothy Jaeryang Baek fac8c3259c refac: rich text input behaviour 2024-11-23 23:57:05 -08:00
Timothy Jaeryang Baek e67fdce727 enh: model editor reset image 2024-11-23 23:26:11 -08:00
Timothy Jaeryang Baek c465a37597 fix: textarea auto height issue 2024-11-23 23:10:12 -08:00
Timothy Jaeryang Baek f5bda4bc27 chore: pyproject bump 2024-11-23 23:00:04 -08:00
Timothy Jaeryang Baek 9ff580b8ca fix: textarea input new line issue 2024-11-23 22:58:09 -08:00
Timothy Jaeryang Baek d3acb5cbaa Merge pull request #7286 from houcheng/tts-aiofiles
Tts aiofiles
2024-11-23 22:37:05 -08:00
Timothy Jaeryang Baek 2a5506a9cd fix: textarea input height issue 2024-11-23 22:27:01 -08:00
Timothy Jaeryang Baek c567185cb1 refac: rich text input behaviour 2024-11-23 20:31:33 -08:00
Timothy Jaeryang Baek 5ed5e532a9 Merge pull request #7279 from Luceurre/fix/missing-tool-description
fix: add missing tool description
2024-11-23 10:39:47 -08:00
houcheng a83f89d430 fix: prevent TTS blocking using aiohttp and aiofiles 2024-11-24 00:28:14 +08:00
Pierre Glandon b2d3bfa3a8 feat: add description in Tool 2024-11-23 11:22:12 +01:00
Timothy Jaeryang Baek 8744a12abb Merge pull request #7274 from DmitriyAlergant-T1A/fix/logging_cleanup
Fix: logging cleanup
2024-11-22 20:16:47 -08:00
DmitriyAlergant-T1A 374d6cad18 Python Formatting (Failed CI - fixed) 2024-11-22 23:11:46 -05:00
DmitriyAlergant-T1A d24c21b40f Fix
Logging cleanup: removed some extraneous hard prints (including some that revealed message content!); improved debug logging a bit.

+ added chat_id to task metadata (helpful for logging/tracking in some pipe functions)
2024-11-22 23:05:45 -05:00
Timothy Jaeryang Baek db929b5d5e Merge pull request #7262 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.4.4
2024-11-22 19:27:41 -08:00
Timothy Jaeryang Baek 47ae5221f7 doc: wording 2024-11-22 19:27:20 -08:00
Timothy Jaeryang Baek 79e988b281 doc: changelogs 2024-11-22 19:26:42 -08:00
Timothy Jaeryang Baek 9412f51c19 chore: version bump 2024-11-22 19:22:35 -08:00
Timothy Jaeryang Baek 320cf06333 Merge pull request #7266 from s1adem4n/update-german-translation
i18n: Add missing German translations and fix existing ones
2024-11-22 16:23:51 -08:00
Jonathan cede8a966f add missing translations and fix existing ones 2024-11-23 01:17:22 +01:00
Timothy Jaeryang Baek c561a4c42b Merge pull request #7263 from michaelpoluektov/fix/docstring-event-emitter
fix: docstring event emitter
2024-11-22 16:06:46 -08:00
Michael Poluektov b4e7957a00 docstring quickfix 2024-11-22 20:51:48 +00:00
Timothy Jaeryang Baek c4eacbfc0f refac: rm print statement 2024-11-22 12:39:08 -08:00
Timothy Jaeryang Baek 429fa2befa fix: query generation 2024-11-22 12:31:06 -08:00
Timothy Jaeryang Baek 3cfd4f8993 fix: controls not being shown in mobile 2024-11-22 11:21:53 -08:00
Timothy Jaeryang Baek 1c3bc99b86 refac: accept legacy 'citation' type 2024-11-22 10:49:30 -08:00
Timothy Jaeryang Baek 335337fc75 Merge pull request #7233 from iamcristi/patch-1
Fix: LDAP integration used None for image and that broke Authentication
2024-11-22 10:42:49 -08:00
Timothy Jaeryang Baek 1ea03cc156 Merge pull request #7228 from OriginalSimon/main
18n: Update Ukrainian translation
2024-11-22 10:42:37 -08:00
Timothy Jaeryang Baek 3923c4df65 Merge pull request #7225 from hilam/translate_pt_br
i18b: Translate missing pt-BR strings and enhancements
2024-11-22 10:42:21 -08:00
Timothy Jaeryang Baek 70ea85484c Merge pull request #7238 from aleixdorca/dev
i18n: Update Catalan translation.json
2024-11-22 10:41:46 -08:00
Timothy Jaeryang Baek 35d75e733d refac: file handling 2024-11-22 10:35:59 -08:00
Aleix Dorca 259881f0b6 Update Catalan translation.json 2024-11-22 17:41:35 +01:00
iamcristi b4cd685795 Update auths.py
changed Auths.insert_new_auth for LDAP accounts. Previously setting image to "None" was causing an error.
2024-11-22 17:23:49 +02:00
SimonOriginal 2d5e1a8c6f fix 2024-11-22 14:51:45 +01:00
SimonOriginal f07ba60f2a Update uk translation.json 2024-11-22 14:43:11 +01:00
Hildeberto e12cf77553 Update pt-BR translation.json 2024-11-22 09:35:25 -03:00
Timothy Jaeryang Baek c13bcfdfc9 Merge pull request #7202 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
chore: version bump
2024-11-21 22:47:45 -08:00
Timothy Jaeryang Baek 9aac02824d chore: version bump 2024-11-21 22:47:29 -08:00
Timothy Jaeryang Baek edd224d542 Merge pull request #7180 from open-webui/dev
0.4.3
2024-11-21 22:46:34 -08:00
Timothy Jaeryang Baek 5c49740aa5 doc: changelog 2024-11-21 22:45:49 -08:00
Timothy Jaeryang Baek 20f31b5bc8 refac 2024-11-21 22:20:57 -08:00
Timothy Jaeryang Baek b23600b49d refac 2024-11-21 22:15:04 -08:00
Timothy Jaeryang Baek 7b9b0f23fe refac 2024-11-21 22:12:40 -08:00
Timothy Jaeryang Baek 2e7af7bdbf fix 2024-11-21 22:03:51 -08:00
Timothy Jaeryang Baek 12c8257c92 refac 2024-11-21 21:32:19 -08:00
Timothy Jaeryang Baek 9a2dd5b126 refac: optimisation 2024-11-21 21:04:35 -08:00
Timothy Jaeryang Baek 4b83a83576 fix: openai connection config 2024-11-21 20:49:40 -08:00
Timothy Jaeryang Baek e18a43aec8 fix: styling 2024-11-21 20:39:00 -08:00
Timothy Jaeryang Baek ea28747baa fix: knowledge base reset 2024-11-21 20:29:54 -08:00
Timothy Jaeryang Baek 3816e3c2ef refac: styling 2024-11-21 20:20:46 -08:00
Timothy Jaeryang Baek 9bc0de2c6a refac 2024-11-21 20:03:30 -08:00
Timothy Jaeryang Baek 81386e9b04 refac: citations -> sources 2024-11-21 19:46:09 -08:00
Timothy Jaeryang Baek 7062e637e8 refac: inline citations 2024-11-21 18:26:38 -08:00
Timothy Jaeryang Baek 714ed248fb refac: styling 2024-11-21 18:05:45 -08:00
Timothy Jaeryang Baek 7b675a1488 refac: styling 2024-11-21 18:02:33 -08:00
Timothy Jaeryang Baek 386c976e9a enh: inline citations 2024-11-21 17:58:29 -08:00
Timothy Jaeryang Baek 5be7cbfdf5 Merge pull request #7182 from michaelpoluektov/fix/tools-metadata
fix: Fix tools metadata
2024-11-21 16:32:08 -08:00
Timothy Jaeryang Baek 0217c044c9 Merge pull request #7195 from michaelpoluektov/remove-unused-router
refactor: Remove unused router
2024-11-21 16:30:22 -08:00
Michael Poluektov 26efc76d40 remove unused router 2024-11-22 00:15:11 +00:00
Michael Poluektov 8abf5d57c1 remove unused schemas file 2024-11-21 23:49:58 +00:00
Timothy Jaeryang Baek 948b65e43f refac 2024-11-21 12:16:50 -08:00
Michael Poluektov e1a85c99ab format 2024-11-21 17:52:19 +00:00
Michael Poluektov 6783c98539 Merge branch 'dev' of https://github.com/open-webui/open-webui into fix/tools-metadata 2024-11-21 17:46:44 +00:00
Michael Poluektov c03bfd141e fix optional args not present 2024-11-21 17:41:35 +00:00
Michael Poluektov 70838148e7 fix tools metadata 2024-11-21 17:19:56 +00:00
Timothy Jaeryang Baek d587206929 refac: add suffix field to ollama generate form 2024-11-21 08:12:19 -08:00
Timothy Jaeryang Baek 1c0327ed7f Merge pull request #7163 from igorbrai/main
feat: Add Mojeek as web search provider
2024-11-21 08:10:14 -08:00
Timothy Jaeryang Baek b0162dfee0 refac 2024-11-21 08:10:05 -08:00
Timothy Jaeryang Baek 1fcde2272b Merge pull request #7162 from morgan55555/dev
LDAP auth fixes: do not store LDAP password, use default user role on first auth, allow ldap only form.
2024-11-21 08:08:47 -08:00
Timothy Jaeryang Baek 48c03ef551 Merge pull request #7147 from aindriu80/main
i18n: Updated Irish translations
2024-11-21 08:06:33 -08:00
Igor Brai d16b09bee5 Merge branch 'open-webui:main' into main 2024-11-21 15:25:41 +02:00
alexey_rechkalov b9e637ee2b Now ENABLE_LOGIN_FORM=False disabling only email form.
LDAP form will be showed instead.
Also added "name" property to inputs for Chrome autocompletion.
2024-11-21 18:11:12 +05:00
alexey_rechkalov b1237cf389 Fixed security vulnerability: now LDAP password hashes are not stored, same as trusted header auth.
LDAP users role now getting DEFAULT_USER_ROLE, not "pending".
2024-11-21 18:09:29 +05:00
Michael Poluektov 3dfea834ca Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-11-21 13:02:39 +00:00
Aindriú Mac Giolla Eoin c03cae811d Updated Irish translations 2024-11-21 10:15:37 +00:00
Timothy Jaeryang Baek 6088acf36d refac: styling 2024-11-20 23:31:46 -08:00
Timothy Jaeryang Baek c5cd1e4403 refac: rich text input 2024-11-20 23:14:06 -08:00
Timothy Jaeryang Baek aca06f92e8 enh: rich text input 2024-11-20 22:56:26 -08:00
Timothy Jaeryang Baek e30c5e628c refac: rich text input 2024-11-20 22:46:51 -08:00
Timothy Jaeryang Baek 6f4bc9864c Merge pull request #7119 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
doc
2024-11-20 12:24:35 -08:00
Timothy Jaeryang Baek 9b03b1a453 doc: readme 2024-11-20 12:24:07 -08:00
Timothy Jaeryang Baek 99c5cf1a00 doc: wording 2024-11-20 12:23:31 -08:00
Timothy Jaeryang Baek 639bea64d8 Merge pull request #7097 from open-webui/dev
0.4.2
2024-11-20 12:15:14 -08:00
Timothy Jaeryang Baek 02d02b0518 doc: changelog 2024-11-20 12:11:38 -08:00
Timothy Jaeryang Baek bc57acb3a6 Merge pull request #7113 from Xelaph/patch-1
**i18n** Update Dutch translation.json
2024-11-20 10:03:04 -08:00
Timothy Jaeryang Baek 2b890cf747 fix: knowledge files issue 2024-11-20 10:02:14 -08:00
Timothy Jaeryang Baek 4156b62811 refac 2024-11-20 10:02:07 -08:00
Timothy Jaeryang Baek c0055afdb3 refac: youtube loader 2024-11-20 10:01:58 -08:00
Alex 882a070cc9 Update translation.json
Added all new translations and changed a lot to better represent Dutch language rules
2024-11-20 18:47:01 +01:00
Timothy Jaeryang Baek d68aa5c708 chore: requirements 2024-11-20 06:53:11 -08:00
Timothy Jaeryang Baek 2450953080 refac 2024-11-20 06:34:52 -08:00
Timothy Jaeryang Baek db1a71c753 fix: usage capability in model editor 2024-11-20 06:24:09 -08:00
Timothy Jaeryang Baek 822a43c53d fix: arena model access control 2024-11-20 06:21:08 -08:00
Timothy Jaeryang Baek d5c65e36c9 fix: prefix issue 2024-11-20 06:12:20 -08:00
Timothy Jaeryang Baek bd1dae2c66 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-11-20 06:09:53 -08:00
Timothy Jaeryang Baek 0dd1d6de2a fix: import 2024-11-20 06:09:48 -08:00
Timothy Jaeryang Baek 505bee066e Merge pull request #7069 from KarlLee830/translate
i18n: Update Chinese translation
2024-11-20 06:07:25 -08:00
Timothy Jaeryang Baek 54cabc9776 Merge pull request #7072 from antpyykk-kone/feature/fix-pipeline-middleware-http-exception-handling
fix: handle uncaught HTTP exceptions in PipelineMiddleware
2024-11-20 06:07:15 -08:00
Antti Pyykkönen 4f6ae8239d fix: handle http exceptions 2024-11-20 09:25:50 +02:00
KarlLee830 3531cf827a i18n: Update Chinese translation 2024-11-20 12:39:22 +08:00
Timothy Jaeryang Baek 02e94c8264 Merge pull request #7057 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.4.1
2024-11-19 20:17:05 -08:00
Timothy Jaeryang Baek 31f00bbc69 doc: changelog 2024-11-19 20:16:34 -08:00
Timothy Jaeryang Baek cb19dbf72b chore: format 2024-11-19 20:15:37 -08:00
Timothy Jaeryang Baek e54879aeb1 enh: detailed 1-10 rating 2024-11-19 20:12:22 -08:00
Timothy Jaeryang Baek 7966367107 refac: models api 2024-11-19 18:45:26 -08:00
Timothy Jaeryang Baek 7c5b845d16 refac: add citations to outlet/actions form
Co-Authored-By: Alex Morgan <alxmrg55@gmail.com>
2024-11-19 18:26:01 -08:00
Timothy Jaeryang Baek cbcd14cdbf doc: changelog 2024-11-19 18:22:45 -08:00
Timothy Jaeryang Baek 0e0a9b4b27 Merge pull request #7061 from zhoubinxin/dev
ci: fix huggingface deploy errors and track .jpg file types
2024-11-19 18:22:35 -08:00
Timothy Jaeryang Baek 70c6d3bf9f fix 2024-11-19 18:21:55 -08:00
Timothy Jaeryang Baek 206a4877c7 fix: api key creation 2024-11-19 18:17:38 -08:00
bx 2c2c2ea9e1 fix(workflow): fix huggingface deploy errors and track .jpg file types 2024-11-20 10:15:40 +08:00
Timothy Jaeryang Baek 7f7b1b18cd doc: changelog 2024-11-19 17:27:34 -08:00
Timothy Jaeryang Baek 00e41790bf enh: show tool description on hover 2024-11-19 17:10:43 -08:00
Timothy Jaeryang Baek 9d9a60afec refac 2024-11-19 16:50:45 -08:00
Timothy Jaeryang Baek eefb2926d8 Merge pull request #7056 from open-webui/dev
fix: deleted user handling
2024-11-19 16:48:30 -08:00
Timothy Jaeryang Baek 0c43c1edf6 fix: deleted user handling 2024-11-19 16:47:35 -08:00
Timothy Jaeryang Baek 3c33432092 Merge pull request #6522 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.4
2024-11-19 14:28:03 -08:00
Timothy Jaeryang Baek 011b8ea3a8 doc: wording 2024-11-19 14:27:43 -08:00
Timothy Jaeryang Baek 321aa2a027 refac: styling 2024-11-19 14:26:53 -08:00
Timothy Jaeryang Baek dd5e500e01 enh: reintroduce update all ollama models feature 2024-11-19 14:23:19 -08:00
Timothy Jaeryang Baek e3b968de26 refac: styling 2024-11-19 14:18:32 -08:00
Timothy Jaeryang Baek 014d5b83d3 doc: wording 2024-11-19 14:13:16 -08:00
Timothy Jaeryang Baek 065517e8b7 chore: bump 2024-11-19 14:05:55 -08:00
Timothy Jaeryang Baek 83f0ab3e10 chore: requirements bump 2024-11-19 14:05:00 -08:00
Timothy Jaeryang Baek d8c5262161 fix: function community import 2024-11-19 14:00:58 -08:00
Timothy Jaeryang Baek 450f5292ef doc: wording 2024-11-19 13:53:10 -08:00
Timothy Jaeryang Baek abe349dab6 doc: changelog 2024-11-19 13:30:23 -08:00
Timothy Jaeryang Baek 8aa2d09cee chore: format 2024-11-19 12:22:58 -08:00
Timothy Jaeryang Baek 0cbb4572f6 Merge pull request #7041 from antpyykk-kone/feature/configure-private-api-key-usage
feat: Ability to configure the use of private API keys in an environment
2024-11-19 12:20:48 -08:00
Timothy Jaeryang Baek 7a585fbaf3 enh: option to disable api auth 2024-11-19 12:17:23 -08:00
Timothy Jaeryang Baek a2476eb1b5 refac: styling 2024-11-19 11:09:02 -08:00
Timothy Jaeryang Baek 81bb2750e0 enh: reset models 2024-11-19 11:03:36 -08:00
Timothy Jaeryang Baek d76e1319ff Merge pull request #7036 from DiegoRodriguez83/#7035
i18n: Brazilian portuguese translation (pt-BR)
2024-11-19 08:13:34 -08:00
Antti Pyykkönen 979e6e5a79 feat: support for configuring private api key use 2024-11-19 16:14:52 +02:00
Diego Rodriguez aa152bd758 i18n: Brazilian portuguese translation (pt-BR) 2024-11-19 08:55:50 -03:00
Timothy Jaeryang Baek 5c4124ebe5 refac 2024-11-19 03:18:40 -08:00
Timothy Jaeryang Baek 53103c3bd7 refac: styling 2024-11-19 03:04:49 -08:00
Timothy Jaeryang Baek 6aa837a8a2 refac: styling 2024-11-19 02:37:27 -08:00
Timothy Jaeryang Baek d2a462a3ad refac 2024-11-19 02:26:12 -08:00
Timothy Jaeryang Baek dbb67a12ca enh: retrieval query generation 2024-11-19 02:24:32 -08:00
Timothy Jaeryang Baek 09c6e4b92f enh: citations option in model editor 2024-11-19 01:12:47 -08:00
Timothy Jaeryang Baek da8f7cff2f refac 2024-11-18 22:49:38 -08:00
Timothy Jaeryang Baek 5ad64c7998 refac 2024-11-18 22:39:55 -08:00
Timothy Jaeryang Baek 67f7ac8bb5 refac 2024-11-18 22:27:42 -08:00
Timothy Jaeryang Baek 6b410ca56a fix: models workspace 2024-11-18 21:10:34 -08:00
Timothy Jaeryang Baek e2d20896b6 enh: model id tooltip in selector 2024-11-18 21:10:28 -08:00
Timothy Jaeryang Baek 6ba2c84c65 fix: styling 2024-11-18 21:00:53 -08:00
Timothy Jaeryang Baek 9ea1c45641 enh: make large text as file toggleable 2024-11-18 20:58:18 -08:00
Timothy Jaeryang Baek 11e78eac91 enh: large pasted text as file
Co-Authored-By: Taylor Wilsdon <6508528+taylorwilsdon@users.noreply.github.com>
2024-11-18 20:50:12 -08:00
Timothy Jaeryang Baek 8bb3061101 Merge pull request #7025 from yeounhak/dev
fix: Enable RAG_WEB_SEARCH_CONCURRENT_REQUESTS
2024-11-18 20:34:27 -08:00
yeounhak 8c161c797b Enable RAG_WEB_SEARCH_CONCURRENT_REQUESTS with asynchronous optimization for improved performance 2024-11-19 01:14:26 +00:00
Timothy Jaeryang Baek e4e4110ec0 refac: config 2024-11-18 14:25:36 -08:00
Timothy Jaeryang Baek d8693c3c74 refac: styling 2024-11-18 14:25:24 -08:00
Timothy Jaeryang Baek 20321e5271 refac: ollama setting for rag 2024-11-18 14:19:56 -08:00
Timothy Jaeryang Baek e3485d2d88 enh: __metadata__ param for tools & functions
Co-Authored-By: DmitriyAlergant-T1A <93501479+dmitriyalergant-t1a@users.noreply.github.com>
2024-11-18 10:12:54 -08:00
Timothy Jaeryang Baek 7da29b6798 fix: rm print statement
Co-Authored-By: Krishan Bhasin <164889026+krishanbhasin-px@users.noreply.github.com>
2024-11-18 10:06:11 -08:00
Timothy Jaeryang Baek 02cafca584 refac 2024-11-18 09:00:51 -08:00
Timothy Jaeryang Baek a77d1aef91 refac: styling 2024-11-18 08:55:09 -08:00
Timothy Jaeryang Baek f37d847521 fix: arena access control 2024-11-18 07:40:37 -08:00
Timothy Jaeryang Baek 269151cd2c enh: arena model access control 2024-11-18 07:18:47 -08:00
Timothy Jaeryang Baek 1ab47a1ff9 refac 2024-11-18 07:06:22 -08:00
Timothy Jaeryang Baek 43f58098f8 refac 2024-11-18 07:05:11 -08:00
Timothy Jaeryang Baek 9525d4279c refac: admin model setting include ollama info 2024-11-18 07:04:21 -08:00
Timothy Jaeryang Baek db6858582f refac: sidebar component 2024-11-18 06:52:41 -08:00
Timothy Jaeryang Baek 8ad1aa3c2e chore: format 2024-11-18 06:39:27 -08:00
Timothy Jaeryang Baek ccdafa8718 refac: styling 2024-11-18 06:38:29 -08:00
Timothy Jaeryang Baek 13a84435e2 refac: styling 2024-11-18 06:34:25 -08:00
Timothy Jaeryang Baek f68e2b2edb refac: styling 2024-11-18 06:32:42 -08:00
Timothy Jaeryang Baek 169ca33b48 refac: styling 2024-11-18 06:24:44 -08:00
Timothy Jaeryang Baek 6dc5efeb19 refac: styling 2024-11-18 06:21:54 -08:00
Timothy Jaeryang Baek c50b678dce enh: tools user info 2024-11-18 06:19:34 -08:00
Timothy Jaeryang Baek 43ffd61aeb refac: model workspace styling 2024-11-18 06:02:14 -08:00
Timothy Jaeryang Baek 6c3e6710ef enh: knowledge author info 2024-11-18 05:51:01 -08:00
Timothy Jaeryang Baek a2a25fb571 enh: models display author name 2024-11-18 05:37:04 -08:00
Timothy Jaeryang Baek 03b606925e refac 2024-11-18 05:24:18 -08:00
Timothy Jaeryang Baek 8eaff8033a refac: arena model modal 2024-11-18 05:22:13 -08:00
Timothy Jaeryang Baek dc8c85c33d fix: shared chat not updating 2024-11-18 05:17:35 -08:00
Timothy Jaeryang Baek bca9c71ed6 refac: styling 2024-11-17 22:09:18 -08:00
Timothy Jaeryang Baek ecf21de28f fix: file drop 2024-11-17 22:06:58 -08:00
Timothy Jaeryang Baek 0faa39ace9 fix 2024-11-17 21:39:52 -08:00
Timothy Jaeryang Baek b0a19d0dda chore: duckduckgo-search bump 2024-11-17 19:20:12 -08:00
Timothy Jaeryang Baek 3faf9d2067 refac: model access control behaviour 2024-11-17 19:15:09 -08:00
Timothy Jaeryang Baek 85731f400c refac: access control 2024-11-17 17:40:31 -08:00
Timothy Jaeryang Baek 07a9b37737 fix: knowledge update issue 2024-11-17 17:37:26 -08:00
Timothy Jaeryang Baek dd8bc65d03 refac 2024-11-17 17:30:53 -08:00
Timothy Jaeryang Baek 0edd4c2263 refac: wording share -> access 2024-11-17 16:13:59 -08:00
Timothy Jaeryang Baek 6589f75b2d fix: ollama custom model 2024-11-17 16:01:47 -08:00
Timothy Jaeryang Baek 3fa23481c8 refac 2024-11-17 15:58:06 -08:00
Timothy Jaeryang Baek 70c9d6fb86 refac: user menu 2024-11-17 15:51:34 -08:00
Timothy Jaeryang Baek 7858be7fda refac: admin models setting toast 2024-11-17 15:50:18 -08:00
Timothy Jaeryang Baek 6906a0bd26 fix: access control 2024-11-17 14:03:51 -08:00
Timothy Jaeryang Baek 17c9a92767 Merge pull request #6996 from Peter-De-Ath/fix_get_groups_by_member_id-postgres-sqlite
fix get groups by member id postgres
2024-11-17 14:01:15 -08:00
Timothy Jaeryang Baek 9779bbfd8e fix: functions redirect 2024-11-17 14:00:37 -08:00
Peter De-Ath 80e2d4d4ee fix: user ID filtering in GroupTable query 2024-11-17 19:53:51 +00:00
Timothy Jaeryang Baek 37f19f68eb refac: user permissions validation 2024-11-17 03:04:31 -08:00
Timothy Jaeryang Baek fbdda55564 fix: styling 2024-11-17 02:58:24 -08:00
Timothy Jaeryang Baek 1d4c3a8c58 fix: access control behaviour 2024-11-17 02:51:57 -08:00
Timothy Jaeryang Baek 892f6ba42b fix: user workspace behaviour 2024-11-17 02:43:43 -08:00
Timothy Jaeryang Baek e65ec1c6cc fix: knowledge 2024-11-17 02:40:47 -08:00
Timothy J. Baek 4a34ca35f0 refac: access control 2024-11-17 01:46:51 -08:00
Timothy Jaeryang Baek a2dcbc41e5 chore: bump 2024-11-17 00:01:20 -08:00
Timothy Jaeryang Baek 0028285361 refac: fuzzy search threshold 2024-11-17 00:00:03 -08:00
Timothy Jaeryang Baek 465e16d282 refac 2024-11-16 23:58:23 -08:00
Timothy Jaeryang Baek c338f2cae1 chore: format 2024-11-16 23:46:12 -08:00
Timothy Jaeryang Baek c24bc60d35 Merge pull request #6989 from Peter-De-Ath/fix-action-button
fix action buttons
2024-11-16 23:44:26 -08:00
Timothy Jaeryang Baek ae2936c8a1 Merge pull request #6981 from aleixdorca/dev
i18n: Update catalan translation.json
2024-11-16 23:44:10 -08:00
Timothy Jaeryang Baek fda3af68ce Merge pull request #6966 from sakiphan/dev
i18n: Update translation.json for Turkish language
2024-11-16 23:43:14 -08:00
Timothy Jaeryang Baek 4e61ff8955 Merge pull request #6949 from oyve/dev
i18n: Update translation.json for Norwegian language
2024-11-16 23:42:36 -08:00
Timothy Jaeryang Baek 24e98bb9d7 Merge pull request #6954 from matthewhand/feature/qdrant-auth
feat: add API key support for Qdrant Cloud authentication
2024-11-16 23:42:16 -08:00
Timothy Jaeryang Baek 034674c19c Merge pull request #6931 from open-webui/groups
feat: user groups
2024-11-16 21:51:42 -08:00
Timothy Jaeryang Baek a47ebd468e refac: dropzone 2024-11-16 21:50:31 -08:00
Timothy Jaeryang Baek d4388a53f4 fix 2024-11-16 21:43:57 -08:00
Timothy Jaeryang Baek f5df0625e3 enh: file upload user permission 2024-11-16 21:41:34 -08:00
Timothy Jaeryang Baek cf2dcf1dc3 feat: user permissions 2024-11-16 21:31:57 -08:00
Timothy Jaeryang Baek 057c957f5d enh: access control 2024-11-16 21:26:10 -08:00
Timothy Jaeryang Baek 07e0712b87 refac: get permissions 2024-11-16 21:16:42 -08:00
Timothy Jaeryang Baek c0371f6525 enh: user permissions 2024-11-16 21:07:56 -08:00
Timothy Jaeryang Baek 79fbab7341 refac 2024-11-16 21:00:20 -08:00
Timothy Jaeryang Baek 19a23a5b7d refac: prompt editor styling 2024-11-16 20:53:14 -08:00
Timothy Jaeryang Baek e78657f07e refac: prompts frontend 2024-11-16 20:53:05 -08:00
Timothy Jaeryang Baek b81cd15ced refac: prompts 2024-11-16 20:52:57 -08:00
Timothy Jaeryang Baek 41bad9abcb refac: access control 2024-11-16 20:47:45 -08:00
Timothy Jaeryang Baek b41e456c4f enh: workspace loading indicator 2024-11-16 18:35:14 -08:00
Timothy Jaeryang Baek b562067e91 refac: tools 2024-11-16 18:31:13 -08:00
Timothy Jaeryang Baek 393ab5d457 refac: tools behaviour 2024-11-16 18:10:01 -08:00
Timothy Jaeryang Baek c35f8c9673 refac: knowledge access control 2024-11-16 18:00:57 -08:00
Timothy Jaeryang Baek 6ebf027613 refac: prompts access control 2024-11-16 18:00:49 -08:00
Timothy Jaeryang Baek 90d283c85e refac: tools access control 2024-11-16 17:57:19 -08:00
Timothy Jaeryang Baek 0a8f69285c refac: toolkit -> tools 2024-11-16 17:54:38 -08:00
Timothy Jaeryang Baek a1ce8422fd refac 2024-11-16 17:49:13 -08:00
Timothy Jaeryang Baek af0ba53715 refac: styling 2024-11-16 17:20:55 -08:00
Timothy Jaeryang Baek 060a2791ba refac: rm print statement 2024-11-16 17:18:18 -08:00
Timothy Jaeryang Baek c3aa09b95c refac 2024-11-16 17:17:18 -08:00
Timothy Jaeryang Baek 73fe77c2da enh: access control 2024-11-16 17:09:15 -08:00
Timothy Jaeryang Baek 227cca35e8 enh: knowledge access control 2024-11-16 16:51:55 -08:00
Timothy Jaeryang Baek 8da24d81a4 refac 2024-11-16 16:01:02 -08:00
Timothy Jaeryang Baek 6ac503413f refac: model tools behaviour 2024-11-16 15:54:07 -08:00
Timothy Jaeryang Baek 7fe10763c1 fix 2024-11-16 15:26:01 -08:00
Timothy Jaeryang Baek c8a3c42ea8 refac: filter out arena models from base models 2024-11-16 15:13:33 -08:00
Timothy Jaeryang Baek 345c9d12a9 refac: migration 2024-11-16 14:56:00 -08:00
Timothy Jaeryang Baek 243e187f0b refac 2024-11-16 14:52:07 -08:00
Peter De-Ath 6332e46e7b fix: update actionMessage to include message parameter 2024-11-16 22:17:55 +00:00
Timothy Jaeryang Baek facb4fdf6b refac 2024-11-16 04:43:10 -08:00
Timothy Jaeryang Baek 932de8f1e2 refac 2024-11-16 04:41:07 -08:00
Timothy Jaeryang Baek 931e03bd9e refac 2024-11-16 02:37:37 -08:00
Timothy Jaeryang Baek 646d926f70 enh: chat file upload permission 2024-11-16 02:31:04 -08:00
Timothy Jaeryang Baek f9412f72f1 refac: styling 2024-11-16 01:54:40 -08:00
Timothy Jaeryang Baek cd655b128f refac 2024-11-16 01:38:20 -08:00
Timothy Jaeryang Baek 240c91e79d wip: access control 2024-11-16 01:24:34 -08:00
Aleix Dorca 2980246cb6 Update catalan translation.json 2024-11-16 07:12:02 +01:00
Timothy Jaeryang Baek 4eb8b1450c refac 2024-11-15 22:09:06 -08:00
Timothy Jaeryang Baek d5f84d6234 refac: model preset handling behaviour 2024-11-15 22:04:33 -08:00
Timothy Jaeryang Baek 19c98b74fa refac: base models endpoint 2024-11-15 19:14:24 -08:00
Timothy Jaeryang Baek 7e78889e33 refac: styling 2024-11-15 18:56:13 -08:00
Timothy Jaeryang Baek 7f8c70b04a wip: admin models setting 2024-11-15 18:53:50 -08:00
Timothy Jaeryang Baek 9dae7d0572 refac: models 2024-11-15 18:21:41 -08:00
Timothy Jaeryang Baek 1c16920dba refac 2024-11-15 17:36:46 -08:00
Sakıp Han Dursun fc789b0cb8 Update translation.json 2024-11-15 15:05:03 +03:00
Timothy Jaeryang Baek 147bd0717d refac 2024-11-15 03:02:08 -08:00
Timothy Jaeryang Baek a0f1164af7 refac 2024-11-15 03:00:18 -08:00
Timothy Jaeryang Baek bf2087c67b refac 2024-11-15 02:06:02 -08:00
Timothy Jaeryang Baek d9dc04f1a1 refac: frontend 2024-11-15 02:05:43 -08:00
Timothy Jaeryang Baek 2ab5b2fd71 wip: access control backend 2024-11-15 01:29:07 -08:00
Timothy Jaeryang Baek b80ec76435 refac 2024-11-14 20:51:49 -08:00
Timothy Jaeryang Baek dff85c733d refac 2024-11-14 20:13:43 -08:00
Timothy Jaeryang Baek 150d0adea2 refac: access_control field 2024-11-14 18:57:25 -08:00
Timothy Jaeryang Baek dae764fa5a feat: groups integration 2024-11-14 18:37:42 -08:00
Timothy Jaeryang Baek 2f893fd373 refac 2024-11-14 18:37:29 -08:00
Timothy Jaeryang Baek 659f3dac44 feat: groups backend 2024-11-14 18:35:14 -08:00
Timothy Jaeryang Baek 716520275b feat: groups db migration 2024-11-14 18:33:16 -08:00
Timothy Jaeryang Baek 947444949a refac: move functions (plugin) to admin panel 2024-11-14 18:32:23 -08:00
Timothy Jaeryang Baek 0cf1969adc refac: rm unused file 2024-11-14 18:31:29 -08:00
Matthew Hand ca63ae649c Enable Qdrant authentication support 2024-11-15 07:06:46 +11:00
Øyvind fb582e44a2 Fix JSON
Update JSON typos
2024-11-14 13:35:32 +01:00
Øyvind 56b0092f43 Update translation.json
Update blanks and polish Norwegian translations
2024-11-14 13:29:01 +01:00
Timothy Jaeryang Baek 73cdf90527 refac: styling 2024-11-14 03:20:35 -08:00
Timothy Jaeryang Baek 85d7f1c6ed refac 2024-11-14 03:16:26 -08:00
Timothy Jaeryang Baek 0e8c0b452e wip: frontend 2024-11-14 02:20:34 -08:00
Timothy Jaeryang Baek aaba41339e Merge pull request #6938 from open-webui/dev
dev
2024-11-13 21:22:42 -08:00
Timothy Jaeryang Baek f5f2215348 enh: sort functions list by type 2024-11-13 21:22:13 -08:00
Timothy Jaeryang Baek ce9aef900c wip: frontend 2024-11-13 21:21:50 -08:00
Timothy Jaeryang Baek 9cdd66ca59 Merge pull request #6909 from sakiphan/dev
i18n- Update the translation of tr-TR
2024-11-13 11:26:31 -08:00
Timothy Jaeryang Baek 4174738b1c Merge pull request #6907 from clang88/patch-1
Update calculatePercentage CitationsModal.svelte (high percentage for high relevance)
2024-11-13 11:21:56 -08:00
Sakıp Han Dursun ee703dfc91 i18n- Update the translation of tr-TR 2024-11-13 17:16:16 +03:00
clang88 67cb3455ad Update CitationsModal.svelte 2024-11-13 14:54:39 +01:00
clang88 ad0b599940 Update calculatePercentage CitationsModal.svelte
Fixes #6904 : Percentage score is high when low relevance and vice-versa.
2024-11-13 14:35:08 +01:00
Timothy Jaeryang Baek baea26d9ca wip: user groups frontend 2024-11-13 03:09:46 -08:00
Timothy Jaeryang Baek 6caf838964 feat: user groups frontend 2024-11-13 02:00:00 -08:00
Timothy Jaeryang Baek 046cf19d27 Merge pull request #6899 from Menghuan1918/dev
i18n: Update the translation of zh-CN
2024-11-13 00:51:06 -08:00
Menghuan1918 baf4d5d7ba i18n: Finish the translation of zh-CN 2024-11-13 16:44:28 +08:00
Timothy Jaeryang Baek 46be83e921 chore: i18n 2024-11-13 00:35:18 -08:00
Timothy Jaeryang Baek 1e7a10842c refac: settings 2024-11-13 00:33:15 -08:00
Timothy Jaeryang Baek f160d278b2 refac: stylign 2024-11-12 23:21:31 -08:00
Timothy Jaeryang Baek 86942d207f refac: icons 2024-11-12 23:17:37 -08:00
Timothy Jaeryang Baek 36d8061b4d refac: evaluations 2024-11-12 23:14:25 -08:00
Timothy Jaeryang Baek 57550cbf56 refac: styling 2024-11-12 23:00:47 -08:00
Timothy Jaeryang Baek 94a0bf90ce refac 2024-11-12 22:50:11 -08:00
Timothy Jaeryang Baek d5435cb8b9 refac: styling 2024-11-12 22:43:18 -08:00
Timothy Jaeryang Baek 17925361dd refac: styling 2024-11-12 22:21:20 -08:00
Timothy Jaeryang Baek ec0680a89f refac: styling 2024-11-12 22:00:37 -08:00
Timothy Jaeryang Baek fae6731137 refac: settings styling 2024-11-12 21:57:41 -08:00
Timothy Jaeryang Baek c260274538 refac: admin users page 2024-11-12 21:51:42 -08:00
Timothy Jaeryang Baek 5a9d61266f refac 2024-11-12 21:09:13 -08:00
Timothy Jaeryang Baek faf3e81bf0 fix 2024-11-12 21:04:07 -08:00
Timothy Jaeryang Baek 81f497c6e3 refac: env var defaults 2024-11-12 20:44:14 -08:00
Timothy Jaeryang Baek 9f58d00727 refac: styling 2024-11-12 20:36:29 -08:00
Timothy Jaeryang Baek 8cfab0216e refac 2024-11-12 20:33:22 -08:00
Timothy Jaeryang Baek 1588477ca3 enh: tool usage indicator 2024-11-12 20:33:12 -08:00
Timothy Jaeryang Baek 760652c4cc refac 2024-11-12 20:26:33 -08:00
Timothy Jaeryang Baek 816b5977a7 Merge pull request #6893 from dannyl1u/enh/settings-search
feat: add more search keywords for settings search
2024-11-12 20:07:24 -08:00
dannyl1u 6bed45a456 enh: add more search keywords for settings search 2024-11-12 19:43:20 -08:00
Timothy Jaeryang Baek 90b40c82dd Merge pull request #6890 from michaelpoluektov/revert-6879-fix/render-html-in-citations
feat: DOMPurify to iframe to render HTML in citations
2024-11-12 16:15:05 -08:00
Timothy Jaeryang Baek 8248e0c216 refac 2024-11-12 16:08:18 -08:00
Michael Poluektov 63ee69a900 sandbox in iframe 2024-11-13 00:07:39 +00:00
Timothy Jaeryang Baek 3d923a15a2 refac 2024-11-12 16:04:03 -08:00
Timothy Jaeryang Baek f470f3fce2 refac 2024-11-12 15:53:16 -08:00
Timothy Jaeryang Baek ee05455155 refac 2024-11-12 15:46:44 -08:00
Timothy Jaeryang Baek 3a541460df refac 2024-11-12 15:32:49 -08:00
Timothy Jaeryang Baek f10d0df490 refac: lazy load prompts/tools/functions/tags 2024-11-12 15:31:11 -08:00
Michael Poluektov f3210ec7db bring html back 2024-11-12 23:17:22 +00:00
Michael Poluektov d0f13d7c07 Revert "fix/feat: add HTML rendering to citations back" 2024-11-12 23:09:18 +00:00
Timothy Jaeryang Baek fb464800e4 Merge pull request #6879 from michaelpoluektov/fix/render-html-in-citations
fix/feat: add HTML rendering to citations back
2024-11-12 14:00:49 -08:00
Timothy Jaeryang Baek b1d31d9b8e refac 2024-11-12 14:00:32 -08:00
Timothy Jaeryang Baek 0f704f1809 refac 2024-11-12 13:57:25 -08:00
Michael Poluektov 330a3535e6 fix: add HTML rendering to citations back 2024-11-12 16:42:17 +00:00
Timothy Jaeryang Baek 5c1b3693cf refac 2024-11-11 23:04:02 -08:00
Timothy Jaeryang Baek 08b5e7ef5b refac: ollama connections 2024-11-11 22:46:47 -08:00
Timothy Jaeryang Baek 99446c4b76 feat: ollama auth support 2024-11-11 22:33:18 -08:00
Timothy Jaeryang Baek 607a8b2109 refac: ollama connections 2024-11-11 22:25:08 -08:00
Timothy Jaeryang Baek f8f36f298a refac 2024-11-11 22:03:57 -08:00
Timothy Jaeryang Baek 3ba7b6f0e5 fix 2024-11-11 21:52:39 -08:00
Timothy Jaeryang Baek 1277579f71 refac 2024-11-11 21:24:30 -08:00
Timothy Jaeryang Baek 0809eb79b8 refac: openai connections 2024-11-11 21:18:51 -08:00
Timothy Jaeryang Baek b82e25cac8 refac: openai /models 2024-11-11 14:11:20 -08:00
Timothy Jaeryang Baek cdc2b4dd61 refac: styling 2024-11-11 13:56:33 -08:00
Timothy Jaeryang Baek 61f29a6531 Merge branch 'main' into dev 2024-11-11 13:45:13 -08:00
Timothy Jaeryang Baek 38a550092e refac: styling 2024-11-11 00:50:43 -08:00
Timothy Jaeryang Baek ad49ad8fa4 refac 2024-11-11 00:49:43 -08:00
Timothy Jaeryang Baek 3b4ee0f127 refac 2024-11-11 00:44:46 -08:00
Timothy Jaeryang Baek b2928975dc refac: styling 2024-11-10 21:01:28 -08:00
Timothy Jaeryang Baek af12f07973 refac 2024-11-10 19:58:49 -08:00
Timothy Jaeryang Baek b903ee835a refac: styling 2024-11-10 19:55:06 -08:00
Timothy Jaeryang Baek 655745ee0c refac: styling 2024-11-10 19:47:55 -08:00
Timothy Jaeryang Baek 3435de5af6 refac: citations 2024-11-10 19:24:53 -08:00
Timothy Jaeryang Baek 7bcb430008 enh: web search indicator 2024-11-10 19:11:06 -08:00
Timothy Jaeryang Baek 19fc83a12b refac: styling 2024-11-10 18:48:54 -08:00
Timothy Jaeryang Baek 317f5df34e refac: settings order 2024-11-10 18:44:52 -08:00
Timothy Jaeryang Baek 466e1fecb4 refac: styling 2024-11-10 18:42:52 -08:00
Timothy Jaeryang Baek 9a546c34bb refac: confirm dialog z index 2024-11-10 18:35:18 -08:00
Timothy Jaeryang Baek 8d1e6f331b refac: prompt editor styling 2024-11-10 18:33:21 -08:00
Timothy Jaeryang Baek 0cc6e20d65 refac: prompt editor 2024-11-10 18:20:46 -08:00
Timothy Jaeryang Baek c92091721b refac: sticky new chat selected models behaviour 2024-11-10 18:01:10 -08:00
Timothy Jaeryang Baek 36712c1597 refac: styling 2024-11-10 17:27:39 -08:00
Timothy Jaeryang Baek c173d275dc Merge pull request #6753 from silentoplayz/silentoplayz-unarchive-all
feat: Unarchive All Archived Chats Button with Confirmation
2024-11-10 16:39:20 -08:00
Timothy Jaeryang Baek f7b98e8c84 refac 2024-11-10 16:39:11 -08:00
Timothy Jaeryang Baek 6ed085b90b chore: format 2024-11-10 16:34:35 -08:00
Timothy Jaeryang Baek be22332fd6 Merge pull request #6841 from konmeo/patch-1
Fix DuckDuckGo's RateLimit
2024-11-10 16:27:15 -08:00
Timothy Jaeryang Baek 24f2152493 Merge pull request #6840 from Peter-De-Ath/fix-translation-uk-UA
Fix missing comma in Ukrainian translation.json
2024-11-10 16:26:55 -08:00
Peter De-Ath 94e432d599 update fomatting and i18n 2024-11-10 22:10:28 +00:00
konmeo e4d35a9478 Fix DuckDuckGo's RateLimit exception (#6624) 2024-11-10 14:05:40 -08:00
Peter De-Ath 638b350b5c Fix missing comma in Ukrainian translation.json 2024-11-10 22:02:03 +00:00
Timothy Jaeryang Baek ff9fdc2f88 refac 2024-11-09 23:57:19 -08:00
Timothy Jaeryang Baek 96c865404d refac: disable openapi.json 2024-11-09 18:01:23 -08:00
Timothy Jaeryang Baek 3ba9f2aae2 Merge pull request #6811 from OriginalSimon/dev
18n: Update Ukrainian translation
2024-11-09 13:58:29 -08:00
Simon 0a027aaa14 Update translation.json 2024-11-09 11:02:05 +01:00
Simon 20d671df39 Update translation.json 2024-11-09 10:59:45 +01:00
Timothy Jaeryang Baek 2fdbab6640 Merge pull request #6761 from diegmonti/feat/permissions-policy
feat: Add permissions-policy to security headers
2024-11-09 00:30:48 -08:00
Timothy J. Baek aa80df1431 refac: styling 2024-11-08 20:14:33 -08:00
Timothy J. Baek f258f44136 refac: styling 2024-11-07 01:30:51 -08:00
Timothy J. Baek cbf6004c76 refac: styling 2024-11-07 01:07:28 -08:00
Timothy J. Baek b1a941ee6f refac: styling 2024-11-07 01:00:30 -08:00
Timothy J. Baek 9ea9e8478a refac: styling 2024-11-07 00:53:39 -08:00
Timothy J. Baek f6fb522269 refac: model editor 2024-11-07 00:18:48 -08:00
Timothy J. Baek a4abd32e4a fix 2024-11-06 23:01:10 -08:00
Timothy J. Baek c9a59f5d51 refac: styling 2024-11-06 22:52:06 -08:00
Timothy J. Baek f8c2bedf53 fix: hybrid search 2024-11-06 22:51:36 -08:00
Timothy J. Baek 24b7716241 refac: workspace styling 2024-11-06 21:45:48 -08:00
Timothy J. Baek d961e8943e refac: styling 2024-11-06 21:13:37 -08:00
Timothy J. Baek 6dc429504a fix: continue message 2024-11-06 20:53:36 -08:00
Timothy Jaeryang Baek 580cf6a02c Merge pull request #6757 from KarlLee830/translate
i18n: Update Chinese translation
2024-11-06 13:54:27 -08:00
Diego Monti b1805380dc feat: Add permissions-policy to security headers 2024-11-06 18:16:22 +01:00
KarlLee830 d88b55e65e i18n: Update Chinese translation 2024-11-06 19:04:07 +08:00
Timothy J. Baek 02269a21a9 refac 2024-11-06 02:58:44 -08:00
Timothy J. Baek ccbf5a08f3 refac 2024-11-06 02:19:04 -08:00
Timothy J. Baek 056f24dc57 refac 2024-11-06 00:32:08 -08:00
silentoplayz f39a523ec0 Update ArchivedChatsModal.svelte
feat: Unarchive All Archived Chats button
2024-11-06 00:58:41 -05:00
Timothy J. Baek 4616b508b1 refac: token handling 2024-11-05 21:14:02 -08:00
Timothy J. Baek 60df959875 chore: format 2024-11-05 21:10:22 -08:00
Timothy J. Baek 926033ba52 chore: format 2024-11-05 21:10:14 -08:00
Timothy J. Baek df85b93b53 refac 2024-11-05 21:05:14 -08:00
Timothy Jaeryang Baek be079e7ea2 Merge pull request #6749 from dannyl1u/feat/settings-search
feat: search in settings
2024-11-05 20:56:35 -08:00
Timothy Jaeryang Baek 69311fec71 Merge pull request #6750 from Sebastriani/main
feat: Possibility to globaly disable Tags generation.
2024-11-05 20:53:10 -08:00
Timothy J. Baek ad462f0a11 refac: ldap settings 2024-11-05 20:52:02 -08:00
Timothy J. Baek 547682c674 refac 2024-11-05 20:47:23 -08:00
Timothy J. Baek 75b169e11d refac: ldap auth page 2024-11-05 20:32:09 -08:00
dannyl1u 9e6595705e feat: settings search - no results found 2024-11-05 20:06:37 -08:00
dannyl1u 2f921859ee lower debounce timer for settings search 2024-11-05 19:59:34 -08:00
Timothy J. Baek 2910cbe9a9 refac: disable ldap by default 2024-11-05 19:58:54 -08:00
dannyl1u 420053b709 run format 2024-11-05 19:16:22 -08:00
dannyl1u 2e645d3303 feat: search in settings 2024-11-05 18:50:58 -08:00
Sebastián D. Gauna e1114bfa4c feat: Possibility to globaly disable Tags generation. 2024-11-05 23:32:08 -03:00
Timothy J. Baek 040449be01 chore: format 2024-11-05 17:41:24 -08:00
Timothy Jaeryang Baek 2d943e784b Merge pull request #6745 from DiegoRodriguez83/#6744
i18n: Translations Brazilian Portuguese (pt-BR)
2024-11-05 17:27:49 -08:00
Diego Rodriguez 739c263e94 i18n: Translations Brazilian Portuguese (pt-BR) 2024-11-05 21:43:18 -03:00
Timothy J. Baek 28b541a692 refac 2024-11-05 15:15:32 -08:00
Timothy Jaeryang Baek 1ee7730fac Merge pull request #6645 from jk-f5/feat/pgvector
feat: Intial support for pgvector as backing vector database
2024-11-05 15:14:55 -08:00
Timothy Jaeryang Baek 1f36cad9ef Fix code scanning alert no. 129: LDAP query built from user-controlled sources
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-11-05 18:10:16 -05:00
Timothy J. Baek fe68a0776c refac
Co-Authored-By: ziweiwang <47988425+wilsonziweiwang@users.noreply.github.com>
2024-11-05 15:09:09 -08:00
Timothy Jaeryang Baek 2777620527 Merge pull request #6743 from morgan55555/dev
feat: LDAP User management
2024-11-05 15:05:50 -08:00
morgan55555 5d934d7d15 feat: LDAP User management
LDAP will be used as default if no other auth form is enabled.
LDAP now will work with ENABLE_LOGIN_FORM = false.
Fixed exception "User does not match the record."
Now LDAP login is case insensitive.
Integrated with onboarding feature.
2024-11-06 03:30:23 +05:00
Timothy J. Baek 2cc2694eba refac: styling 2024-11-05 14:05:13 -08:00
Timothy Jaeryang Baek 3e52e95b2c Merge pull request #6724 from silentoplayz/silentoplayz-tooltips
feat: Add tooltips to provide contextual information for each parameter
2024-11-05 13:59:51 -08:00
Timothy Jaeryang Baek b6bebd76f0 Merge pull request #6737 from aleixdorca/dev
i18n: Updated the catalan translation.json
2024-11-05 12:59:55 -08:00
silentoplayz cd6e05d43c Update AdvancedParams.svelte
fix
2024-11-05 13:10:51 -05:00
Aleix Dorca 665cdffedc Update catalan translation.json 2024-11-05 18:53:26 +01:00
silentoplayz 634be4a97a Update AdvancedParams.svelte
i18n.t
2024-11-05 12:51:40 -05:00
silentoplayz e5f9ef4515 Update AdvancedParams.svelte
Added missing tooltips
2024-11-05 12:27:00 -05:00
Timothy J. Baek eb56614b58 refac: wording 2024-11-05 00:18:35 -08:00
silentoplayz 8c1983cfda Update AdvancedParams.svelte
Added Advanced Parameters tooltips
2024-11-05 01:39:54 -05:00
Timothy J. Baek d4b60b1bac refac: styling 2024-11-04 19:52:12 -08:00
Timothy Jaeryang Baek ea1d512ee3 Merge pull request #6719 from DiegoRodriguez83/#6717
i18n: Added labels in 2 components + translation and adjustments for pt-BR
2024-11-04 19:36:25 -08:00
Timothy Jaeryang Baek c29b283f5d Merge branch 'dev' into #6717 2024-11-04 19:35:49 -08:00
Timothy J. Baek 3afc34928f refac: onboarding 2024-11-04 19:31:56 -08:00
Diego Rodriguez 573b63c06b CHANGELOG 2024-11-04 23:52:08 -03:00
Diego Rodriguez 7aeedda634 i18n: Implementing label [SearchInput] and [CreateCollection] | Translation and adjustments pt-BR 2024-11-04 23:37:53 -03:00
Jason Kidd 319ea8cb7f feat: Add ability to set URI for pgvector 2024-11-04 14:27:33 -08:00
Jason Kidd 701f40aedd feat: Initial support for pgvector 2024-11-04 14:26:41 -08:00
Timothy J. Baek 5d3da6dcc8 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-11-04 14:25:41 -08:00
Timothy J. Baek 88ea53bd6a fix 2024-11-04 14:25:35 -08:00
Timothy Jaeryang Baek 6c1d0a8e39 Merge pull request #6598 from dtaivpp/main
feat: OpenSearch vector db support
2024-11-04 14:04:55 -08:00
Timothy J. Baek 7c0545fd39 chore: requirements 2024-11-04 14:02:55 -08:00
Timothy J. Baek 224b555de5 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-11-04 14:01:26 -08:00
Timothy J. Baek 151b3cf8e7 chore: i18n 2024-11-04 14:01:19 -08:00
Timothy Jaeryang Baek 8234036001 Merge pull request #6701 from davizucon/dev
Small Fix: variable from ssl_verification to: web_loader_ssl_verification
2024-11-04 14:00:25 -08:00
David Tippett 705e3129b6 Updating config format. 2024-11-04 15:14:53 -05:00
David Tippett afca48028f Moving new config to under retreval 2024-11-04 15:10:14 -05:00
David Tippett 4baab4bce8 Merge branch 'open-webui:main' into main 2024-11-04 15:04:30 -05:00
David Tippett 16089ab947 Fix formatting in pyproject.toml dependencies list 2024-11-04 14:59:50 -05:00
Davi Sclifo Zucon a70e5a0f30 When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False 2024-11-04 16:29:29 -03:00
Davi Sclifo Zucon 35a9140567 Added parameter to: app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION to get_web_loader(verify_ssl).
Invert variable value to send to UI , not ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION.
2024-11-04 16:21:26 -03:00
Davi Sclifo Zucon 5306b49be0 Small Fix variable ssl_verification -> web_loader_ssl_verification (following python entity)
When changed via the web interface in Admin's WebSearch, the bypass option is not being persisted.
This change adjusts it to the same name as the one related in the python entity.
2024-11-04 15:40:16 -03:00
Timothy J. Baek 0b47387081 refac: styling 2024-11-04 02:32:06 -08:00
Timothy J. Baek 630d1397ad refac: styling 2024-11-04 01:57:01 -08:00
Timothy J. Baek 1fd67d7e5d feat: native speecht5 support 2024-11-04 01:16:51 -08:00
Timothy J. Baek 835eeb6433 fix: playground styling 2024-11-03 23:05:41 -08:00
Timothy J. Baek 5c60081c1f refac: web search 2024-11-03 17:07:24 -08:00
Timothy J. Baek 70498d7bbe enh: export table as csv
Co-Authored-By: Muhammad Afzaal <mafzaal@gmail.com>
2024-11-03 14:09:57 -08:00
Timothy J. Baek 5ae6d05a53 fix 2024-11-03 04:05:46 -08:00
Timothy J. Baek f1065115aa fix: youtube loader issue 2024-11-03 03:18:17 -08:00
Timothy J. Baek d3ac439000 refac: styling 2024-11-03 03:04:35 -08:00
Timothy J. Baek 194b924a96 feat: onboarding screen 2024-11-03 03:00:28 -08:00
Timothy J. Baek cfe255bb95 refac: styling 2024-11-03 02:31:12 -08:00
Timothy J. Baek dbf14afa32 refac: styling 2024-11-03 02:00:31 -08:00
Timothy J. Baek 330ad0131c refac 2024-11-03 01:39:03 -08:00
Timothy J. Baek 39bfc4c8e9 refac 2024-11-03 01:38:07 -08:00
Timothy J. Baek 4e7951d5fc fix: allow openai list message format 2024-11-03 01:34:45 -08:00
Timothy J. Baek 0925c6b33b refac: remove unused dependency 2024-11-03 01:26:16 -08:00
Timothy J. Baek 98cea8aaa4 refac: automatically disable new sign ups after first user 2024-11-03 01:25:41 -08:00
Timothy J. Baek 503019b28d fix: web stt issue 2024-11-03 01:21:58 -08:00
Timothy J. Baek 380e080f6d fix: do not crash on invalid embedding model 2024-11-03 01:08:04 -08:00
Timothy J. Baek 6027c13227 fix: s3 file path 2024-11-03 01:07:43 -08:00
Timothy J. Baek 0c6453078e refac: orientation 2024-11-03 01:59:53 -07:00
Timothy J. Baek 230e2f8f82 fix: remove tags gen print 2024-11-03 01:54:23 -07:00
Timothy J. Baek 8c5206018c fix: respect case sensitivity 2024-11-03 01:43:14 -07:00
Timothy Jaeryang Baek c315a7d9dd Merge pull request #6631 from open-webui/dependabot/pip/backend/dev/python-multipart-0.0.17
build(deps): bump python-multipart from 0.0.9 to 0.0.17 in /backend
2024-11-02 23:25:25 -07:00
Timothy Jaeryang Baek 01887098ac Merge pull request #6629 from open-webui/dependabot/pip/backend/dev/boto3-1.35.53
build(deps): bump boto3 from 1.35.0 to 1.35.53 in /backend
2024-11-02 23:25:09 -07:00
Timothy Jaeryang Baek e89e17b654 Merge pull request #6628 from open-webui/dependabot/pip/backend/dev/pymilvus-2.4.9
build(deps): bump pymilvus from 2.4.7 to 2.4.9 in /backend
2024-11-02 23:24:57 -07:00
Timothy Jaeryang Baek dead5cdd4e Merge pull request #6589 from DucNgn/dn_forward_user_info_in_headers
feat: Add option to forward user info as headers to OpenAI API
2024-11-02 17:24:04 -07:00
David Tippett ef7a9e2467 Update chromadb and pymilvus versions
Fixing botched merge change.
2024-11-01 21:47:05 -04:00
Timothy Jaeryang Baek c44036e2b8 Merge pull request #6646 from s1adem4n/fix-popover-text
fix: Truncate text in UserMenu so it doesn't break into multiple lines
2024-11-01 18:22:21 -07:00
Timothy Jaeryang Baek f23ae8f7a0 Merge pull request #6636 from aindriu80/main
i18n: Translated new strings into Irish, edited some existing strings
2024-11-01 18:17:49 -07:00
Timothy Jaeryang Baek 2bace37890 Merge pull request #6644 from s1adem4n/improve-german-translation
i18n: Improve German translation, add missing strings
2024-11-01 18:17:34 -07:00
Timothy Jaeryang Baek 692c3f992f Merge pull request #6635 from ahangarha/improve-persian-translation
i18n: Improve persian (fa-IR) translation
2024-11-01 18:17:22 -07:00
Timothy Jaeryang Baek 89f62baa08 Merge pull request #6633 from t0ny-d3v/dev
Dev: cs-CZ fix translations
2024-11-01 18:17:07 -07:00
Jonathan a8dfc0091c truncate text so it doesnt break and look weird 2024-11-02 00:32:12 +01:00
Jonathan 6e1b89b55a improve german translation 2024-11-01 23:18:17 +01:00
Mostafa Ahangarha b8304d13a9 Improve persian (fa-IR) translation 2024-11-01 22:06:28 +03:30
Aindriú Mac Giolla Eoin dad12067d8 Translated new strings into Irish, edited some existing strings 2024-11-01 17:25:49 +00:00
Duc Nguyen 06f44dc067 Minor comment 2024-11-01 11:24:10 -04:00
Duc Nguyen a34d202ae8 move the config to env.py 2024-11-01 11:23:18 -04:00
Jozef Papai 9579208b37 Update translation.json
Update some texts
2024-11-01 11:25:02 +02:00
Jozef Papai 31348aa162 Update translation.json
Fix some translatios
2024-11-01 11:17:48 +02:00
dependabot[bot] 705316aef3 build(deps): bump python-multipart from 0.0.9 to 0.0.17 in /backend
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.9 to 0.0.17.
- [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.9...0.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-01 02:25:46 +00:00
dependabot[bot] 39f841ce34 build(deps): bump boto3 from 1.35.0 to 1.35.53 in /backend
Bumps [boto3](https://github.com/boto/boto3) from 1.35.0 to 1.35.53.
- [Release notes](https://github.com/boto/boto3/releases)
- [Commits](https://github.com/boto/boto3/compare/1.35.0...1.35.53)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-01 02:25:39 +00:00
dependabot[bot] b6372a4cb2 build(deps): bump pymilvus from 2.4.7 to 2.4.9 in /backend
Bumps [pymilvus](https://github.com/milvus-io/pymilvus) from 2.4.7 to 2.4.9.
- [Release notes](https://github.com/milvus-io/pymilvus/releases)
- [Commits](https://github.com/milvus-io/pymilvus/compare/v2.4.7...v2.4.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-01 02:25:32 +00:00
Timothy Jaeryang Baek c3faed71ef Merge pull request #6611 from open-webui/main
dev
2024-10-30 21:08:48 -07:00
Timothy Jaeryang Baek 255109d341 Merge pull request #6604 from Peter-De-Ath/update-langchain-community
fix/enh: update langchain-community
2024-10-30 16:13:42 -07:00
Peter De-Ath 7c6fa64c05 fix/enh: update langchain-community 2024-10-30 22:46:06 +00:00
David Tippett 6aec971ddb Updating requirements.txt 2024-10-30 15:53:21 -04:00
David Tippett a39ddfd4f1 Adding OpenSearch python dependancy 2024-10-30 15:52:41 -04:00
David Tippett fb30b667e2 First pass at an OpenSearch connector 2024-10-30 15:51:56 -04:00
Timothy Jaeryang Baek 9d6cfe64ba Merge pull request #6585 from t0ny-d3v/dev
Rename cs_CZ to cs-CZ
2024-10-30 10:28:17 -07:00
Timothy J. Baek 7228b39064 Update README.md 2024-10-30 10:16:28 -07:00
Duc Nguyen 3f0b3ea90e feat: Add option to forward user info as headers to OpenAI API 2024-10-30 11:29:18 -04:00
Jozef Papai 91180864a7 Rename cs_CZ to cs-CZ
Fix dir name
2024-10-30 09:16:26 +02:00
Timothy Jaeryang Baek 785497a34a Merge pull request #6567 from open-webui/dependabot/pip/pip-1185b3d527
build(deps): bump the pip group across 2 directories with 1 update
2024-10-29 23:27:03 -07:00
Timothy Jaeryang Baek adfce28732 Merge pull request #6543 from execgit/main
fix: omit document contents in logging document embedding
2024-10-29 19:17:53 -07:00
Timothy Jaeryang Baek 4765dfd28c Merge pull request #6574 from mafzaal/ur-PK-translation_v2
i18n - Urdu PK translation
2024-10-29 17:10:38 -07:00
Muhammad Afzaal a2d72deaae Added Urdu Pakistan (ur-PK) translations 2024-10-29 18:13:56 -05:00
dependabot[bot] 3a713aea18 build(deps): bump the pip group across 2 directories with 1 update
Bumps the pip group with 1 update in the / directory: [langchain](https://github.com/langchain-ai/langchain).
Bumps the pip group with 1 update in the /backend directory: [langchain](https://github.com/langchain-ai/langchain).


Updates `langchain` from 0.2.15 to 0.3.0
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain==0.2.15...langchain==0.3.0)

Updates `langchain` from 0.2.15 to 0.3.0
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain==0.2.15...langchain==0.3.0)

---
updated-dependencies:
- dependency-name: langchain
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: langchain
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-29 20:23:44 +00:00
Timothy Jaeryang Baek 4cf5a165bc Merge pull request #6537 from t0ny-d3v/main
i18n Add new language - CZ, add missing translations to RO
2024-10-29 10:04:24 -07:00
Igor Brai b7d730e244 add mojeek as websearch option 2024-10-29 16:45:38 +02:00
execgit 03e9add96d retrieval.utils: omit logging file contents at level INFO 2024-10-29 14:33:37 +02:00
execgit bc7622c0fe Avoid logging file contents at level INFO
I had problems with document handling in rootless containers. Long documents caused the container to hang. Reducing the verbosity of logging from retrieval.main seemed to fix the issues I was experiencing.
2024-10-29 14:31:47 +02:00
Timothy Jaeryang Baek b36a1eef4a Merge pull request #6523 from open-webui/main
refac: feedback base_models
2024-10-28 15:21:07 -07:00
Timothy J. Baek 09935d191f refac: feedback base_models 2024-10-28 15:19:02 -07:00
Timothy Jaeryang Baek 489a69673f Merge pull request #6521 from open-webui/main
enh: include base model id in feedback metadata
2024-10-28 15:09:31 -07:00
Timothy Jaeryang Baek 46b2505fbd Merge pull request #6520 from turbra/main
fix: automatic1111 typo
2024-10-28 15:09:01 -07:00
Timothy J. Baek e03e22dd1f enh: include base model id in feedback metadata 2024-10-28 15:07:40 -07:00
turbra 572526c1bb Update config.py 2024-10-28 17:02:18 -04:00
Timothy J. Baek 8f2f4747e7 fix 2024-10-28 13:29:44 -07:00
Timothy Jaeryang Baek b32fbf7c58 Merge pull request #6504 from jeeteshchel/fork/dev
feat: enable bing support
2024-10-28 12:30:11 -07:00
Jeetesh Chellani f7d8a6ccba feat: enable bing support 2024-10-28 16:51:31 +02:00
Timothy Jaeryang Baek 90759bc89a Merge pull request #6495 from andrewbbaek/dev
Korean Translation Update
2024-10-28 05:52:28 -07:00
Timothy J. Baek 491cff8c63 refac: parse md files with TextLoader 2024-10-28 05:50:56 -07:00
Timothy J. Baek bcf88585de fix: pdf issue w/ markdown 2024-10-28 05:47:53 -07:00
Timothy J. Baek 25a2c6ee52 fix/refac: use user prompt for title gen fallback 2024-10-28 05:34:50 -07:00
Jozef Papai 046d8d04aa Update translation.json
added missing translations
2024-10-28 11:40:23 +02:00
Jozef Papai 4a4dce3a33 Update languages.json
Add czech
2024-10-28 11:16:11 +02:00
Jozef Papai 88c27dc1df Create translation.json
Czech translate file
2024-10-28 11:15:23 +02:00
Andrew Baek dd88882ad4 Update translation.json 2024-10-28 15:04:32 +09:00
Andrew Baek b92d530cf3 Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2024-10-28 14:09:56 +09:00
Timothy Jaeryang Baek a64cc8f8e0 Merge pull request #6474 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-27 16:27:03 -07:00
Timothy Jaeryang Baek 9a81e34fe1 Merge pull request #6473 from HaldiH/patch-1
fix: generate endpoint form model
2024-10-27 16:26:51 -07:00
Timothy Jaeryang Baek 206a9cba93 Merge pull request #6470 from Xelaph/patch-1
i18n: Added and changed Dutch translations
2024-10-27 16:26:13 -07:00
Timothy Jaeryang Baek b4d116475b Merge pull request #6468 from diwakar-s-maurya/main
feat: support authentication for chromadb
2024-10-27 16:25:55 -07:00
KarlLee830 38ae5cc0d5 i18n: Update Chinese translation 2024-10-27 20:58:37 +08:00
Hugo Haldi b596b8f0cb Fix: generate endpoint form model
According to the [Ollama documentation] the parameter `context` should be an array of int, if specified.
2024-10-27 13:41:50 +01:00
Andrew Baek 9c299625d3 Update translation.json (ko-KR)
ko-KR
2024-10-27 20:42:14 +09:00
Alex 1eeac56ce2 Merge branch 'open-webui:main' into patch-1 2024-10-27 09:53:10 +01:00
Alex bc6104a6f9 Update translation.json
Added last translations and changed some into clearer or better language (in my opinion)
2024-10-27 09:46:43 +01:00
Alex fd9c3b0a5b Update translation.json
Added more translations
2024-10-27 09:27:05 +01:00
Diwakar Singh Maurya 3d0c32f366 feature to support authentication in chromadb 2024-10-27 14:01:00 +07:00
Timothy J. Baek e35be175fc fix 2024-10-26 22:05:19 -07:00
Timothy J. Baek effcbd6301 feat: airdrop chats between windows 2024-10-26 21:25:48 -07:00
Timothy J. Baek e6ca994c92 feat: option to disable what's new modal 2024-10-26 19:44:46 -07:00
Timothy Jaeryang Baek 21b8ca3459 Merge pull request #6465 from open-webui/dev
refac: migration
2024-10-26 19:01:32 -07:00
Timothy J. Baek 5537b2dc2c refac: migration 2024-10-26 19:01:01 -07:00
Alex 5542a9707c Update translation.json
Added what I could easily translate and changed some translations that were obviously wrong
2024-10-26 23:29:53 +02:00
Timothy Jaeryang Baek f1f068f458 Merge pull request #6460 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.35
2024-10-26 13:17:35 -07:00
Timothy J. Baek 53e772df2f doc: changelog 2024-10-26 13:16:49 -07:00
Timothy J. Baek e36acd6217 enh: empty file handling behaviour 2024-10-26 13:05:54 -07:00
Timothy J. Baek 25a37baeec chore: format 2024-10-26 13:05:37 -07:00
Timothy Jaeryang Baek dab2308fe2 Merge pull request #6450 from superzazu/fr_fixes
i18n: update french translation fr-FR
2024-10-26 12:58:14 -07:00
Timothy J. Baek 15cfdc69a8 refac: chat input file handling 2024-10-26 12:56:37 -07:00
Timothy Jaeryang Baek 032abba88e Merge pull request #6458 from duroakos/dev
i18n: added hungarian translation hu-HU
2024-10-26 12:43:35 -07:00
Timothy J. Baek f03629de61 fix: merged response should be used in messages payload 2024-10-26 12:41:54 -07:00
Timothy J. Baek badbe4ea06 fix: tools not working 2024-10-26 12:21:05 -07:00
duroakos c162e7074e i18n: added hungarian translation hu-Hu 2024-10-26 21:14:08 +02:00
Nicolas Allemand f5c74837b1 i18n: update french translation fr-FR 2024-10-26 18:50:28 +02:00
Timothy Jaeryang Baek 565ea92693 Merge pull request #6434 from open-webui/main
dev
2024-10-26 01:50:40 -07:00
Timothy J. Baek d056923479 Update CHANGELOG.md 2024-10-26 01:45:30 -07:00
Timothy Jaeryang Baek 1cd7d83f50 Merge pull request #6433 from open-webui/dev
fix: knowledge issue
2024-10-26 01:19:38 -07:00
Timothy J. Baek f1c123c819 fix: knowledge issue 2024-10-26 01:19:17 -07:00
Timothy Jaeryang Baek f10c729e3d Merge pull request #6413 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.34
2024-10-26 00:44:16 -07:00
Timothy J. Baek 488d7f10b3 doc: wording 2024-10-26 00:44:08 -07:00
Timothy J. Baek bbc7e2776d doc: changelog 2024-10-26 00:42:27 -07:00
Timothy J. Baek 07c9ace061 chore: version bump 2024-10-26 00:27:35 -07:00
Timothy J. Baek b6f548035c chore: format 2024-10-26 00:24:10 -07:00
Timothy Jaeryang Baek 554e181ccb Merge pull request #6432 from diwakar-s-maurya/main
fix: Prevent crash while setting Strict-Transport-Security security header
2024-10-26 00:23:15 -07:00
Timothy J. Baek 0cf8f58efe refac: voice input styling 2024-10-26 00:21:46 -07:00
Diwakar 0a7bc50279 Prevent crash while setting Strict-Transport-Security security header 2024-10-26 13:45:28 +07:00
Timothy J. Baek 1cd036e768 refac 2024-10-25 22:33:26 -07:00
Timothy J. Baek f2c78ac0fb refac 2024-10-25 22:23:21 -07:00
Timothy J. Baek 925a903e38 chore: format 2024-10-25 22:18:48 -07:00
Timothy J. Baek 47e4250f58 refac 2024-10-25 22:12:46 -07:00
Timothy J. Baek 21a28e3bf0 enh: more robust knowledge file info retrieval 2024-10-25 21:58:29 -07:00
Timothy J. Baek 5d2714bc92 fix: s3 file upload 2024-10-25 21:50:31 -07:00
Timothy J. Baek 076c54c486 refac 2024-10-25 21:47:47 -07:00
Timothy J. Baek 780591e991 refac 2024-10-25 21:46:14 -07:00
Timothy J. Baek 50dcad0f73 fix: tiktoken encoding model issue 2024-10-25 21:38:28 -07:00
Timothy J. Baek adede5480d enh: rich text input toggle option 2024-10-25 21:31:18 -07:00
Timothy J. Baek 8b61b39c75 enh: feedback exports 2024-10-25 21:17:47 -07:00
Timothy J. Baek 58d929fd65 enh: lazy load embedding model for leaderboard 2024-10-25 21:07:16 -07:00
Timothy Jaeryang Baek 04babeb45a Merge pull request #6424 from Peter-De-Ath/add-user-name-slash-prompt
feat: add user_name to slash prompt template
2024-10-25 18:20:27 -07:00
Peter De-Ath 5c3c00f9fb add user_name slash prompt template 2024-10-26 00:37:38 +01:00
Timothy Jaeryang Baek 3df1ee9954 Merge pull request #6423 from Peter-De-Ath/fix-chat-rename-firefox
fix: folder rename not save on enter - firefox
2024-10-25 15:21:09 -07:00
Peter De-Ath b0e83ccea0 Merge remote-tracking branch 'personal/fix-chat-rename-firefox' into dev 2024-10-25 23:14:14 +01:00
Peter De-Ath 40764d91cf fix: folders not renamed on enter key 2024-10-25 22:54:12 +01:00
Timothy J. Baek 37853d6a26 enh: more robust tool calling 2024-10-25 14:36:44 -07:00
Timothy J. Baek 0b38584e52 refac: rich text input paste behaviour 2024-10-25 11:51:49 -07:00
Timothy Jaeryang Baek af3456511b Merge pull request #6409 from MarcRevo/nl-translations-mb
i18n: Update Dutch Translation nl-NL
2024-10-25 11:15:13 -07:00
Timothy Jaeryang Baek 9b030f6ad6 Merge pull request #6392 from nodomain/patch-1
fix: boolean values don't need to be accessed by .value
2024-10-25 11:12:53 -07:00
Timothy Jaeryang Baek ad952b9394 Merge pull request #6399 from Natedorr/main
Update api/chat stream to default to true
2024-10-25 11:12:29 -07:00
MarcRevo a1469cae59 Add: i18n Dutch translations 2024-10-25 19:50:51 +02:00
MarcRevo bbc98259a5 Add: nl-NL Translations 2024-10-25 19:45:46 +02:00
MarcRevo 44433929e7 Modify and Add Dutch translations 2024-10-25 19:33:05 +02:00
Nate.Dorr b4acf689e3 update the GenerateChatCompletionForm stream to be defaulted to true.
This defaults the /api/chat/ endpoint to default to streaming being true
2024-10-25 08:00:37 -05:00
Fabian Fischer 23461332b9 fix: boolean values don't need to be accessed by .value 2024-10-25 09:44:03 +02:00
Timothy J. Baek 46a97e62c7 fix: tags custom prompt issue 2024-10-25 00:05:04 -07:00
Timothy J. Baek d905bda000 refac: template notes 2024-10-24 16:46:35 -07:00
Timothy Jaeryang Baek 144dc0ed2f Merge pull request #6383 from open-webui/dev
refac
2024-10-24 15:04:36 -07:00
Timothy J. Baek 1472f12f5e refac 2024-10-24 15:02:26 -07:00
Timothy Jaeryang Baek 68916f7ec4 Merge pull request #6382 from open-webui/dev
fix
2024-10-24 14:57:25 -07:00
Timothy J. Baek 4282b20495 fix 2024-10-24 14:53:14 -07:00
Timothy Jaeryang Baek 9b12b75df6 Merge pull request #6380 from open-webui/dev
fix: arena model exclude filter
2024-10-24 14:23:26 -07:00
Timothy J. Baek 856c00bc2f fix: arena model exclude filter 2024-10-24 14:22:04 -07:00
Timothy Jaeryang Baek 99dd7fb5a8 Merge pull request #6002 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.33
2024-10-24 13:36:19 -07:00
Timothy J. Baek e9e49babf8 enh: remove token cookie on signout 2024-10-24 13:35:29 -07:00
Timothy J. Baek 6b620d93a9 refac 2024-10-24 03:13:35 -07:00
Timothy J. Baek c5d974606f refac 2024-10-24 03:10:17 -07:00
Timothy J. Baek 587dab23f0 fix 2024-10-24 02:35:17 -07:00
Timothy J. Baek a4f29e1e55 chore: format 2024-10-24 02:26:52 -07:00
Timothy J. Baek 205402e096 refac 2024-10-24 01:01:00 -07:00
Timothy J. Baek 42a58da487 refac 2024-10-24 00:56:24 -07:00
Timothy J. Baek d160d0351f refac 2024-10-24 00:48:34 -07:00
Timothy J. Baek 744139d2ce refac 2024-10-24 00:35:27 -07:00
Timothy J. Baek a28ef8acc5 refac 2024-10-24 00:34:44 -07:00
Timothy J. Baek 9c2d592c73 enh: include message_turn metadata 2024-10-24 00:23:31 -07:00
Timothy J. Baek 160e63e509 refac: feedback history share support 2024-10-24 00:00:49 -07:00
Timothy J. Baek 056950b4f9 enh: leaderboard notice 2024-10-23 23:07:44 -07:00
Timothy J. Baek d7151fe6fa refac: similarity operation 2024-10-23 22:38:58 -07:00
Timothy J. Baek cde33002c7 feat: topic leaderboard 2024-10-23 22:35:12 -07:00
Timothy J. Baek 0f4b6cdb67 refac 2024-10-23 20:49:18 -07:00
Timothy J. Baek d0662e2518 refac: do not use arena model for tasks 2024-10-23 20:33:08 -07:00
Timothy J. Baek ec37d801be refac: feedback optimisation 2024-10-23 20:27:32 -07:00
Timothy J. Baek b5d7e57d3c refac 2024-10-23 20:19:42 -07:00
Timothy J. Baek 076f9fd9c0 enh: auto tag feedback 2024-10-23 20:18:51 -07:00
Timothy J. Baek 73ab84efe6 fix: leaderboard
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2024-10-23 18:54:49 -07:00
Timothy J. Baek 0c6e9e3e86 refac: rating 2024-10-23 16:59:54 -07:00
Timothy J. Baek c1a054de4a fix 2024-10-23 15:35:40 -07:00
Timothy J. Baek 51265b4683 refac 2024-10-23 15:33:13 -07:00
Timothy J. Baek d4c42dcfa9 fix 2024-10-23 15:09:20 -07:00
Timothy J. Baek d85480b4d6 fix: bypass_filter for arena models 2024-10-23 15:05:43 -07:00
Timothy J. Baek ee2f8d3552 fix: folder migration 2024-10-23 14:58:28 -07:00
Timothy J. Baek 0e56ef20cb fix: selectedModels 2024-10-23 14:51:25 -07:00
Timothy J. Baek b16c4bc1df refac 2024-10-23 12:41:41 -07:00
Timothy J. Baek 68a47f6a9f enh: arena model filter 2024-10-23 12:40:47 -07:00
Timothy J. Baek affb0e5c37 refac 2024-10-23 12:33:00 -07:00
Timothy Jaeryang Baek 15c3bdf7af Merge pull request #6361 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-23 10:03:45 -07:00
KarlLee830 42914a162f i18n: Update Chinese translation 2024-10-23 21:46:28 +08:00
Timothy Jaeryang Baek 928a5e747c Merge pull request #6358 from OriginalSimon/dev
18n: Update Ukrainian translation
2024-10-23 05:04:48 -07:00
Simon a494ef8a99 Update translation.json 2024-10-23 13:50:56 +02:00
Timothy J. Baek 8d2089e5b8 refac: styling 2024-10-23 02:17:10 -07:00
Timothy J. Baek 4557f4d165 chore: version bump 2024-10-23 02:15:11 -07:00
Timothy J. Baek b9af35be4c refac 2024-10-23 01:48:26 -07:00
Timothy J. Baek a41a5cc0c8 refac 2024-10-23 01:27:32 -07:00
Timothy J. Baek 4a1bf6d6c4 refac 2024-10-23 01:23:49 -07:00
Timothy J. Baek 0d924a1cba enh: feedback delete support 2024-10-23 01:05:45 -07:00
Timothy J. Baek 01651ee0a6 enh: feedback history pagination 2024-10-23 00:51:27 -07:00
Timothy J. Baek 3531e009ce chore: format 2024-10-23 00:42:13 -07:00
Timothy Jaeryang Baek 6edbbde2eb Merge pull request #6348 from MadsLang/main
Adding Danish language
2024-10-23 00:39:31 -07:00
Timothy J. Baek 8514ed148a refac: show changelog behaviour 2024-10-23 00:38:59 -07:00
Timothy J. Baek 2897c25b99 refac 2024-10-22 23:55:36 -07:00
Timothy J. Baek 58261a7b83 refac: styling 2024-10-22 23:54:37 -07:00
Timothy Jaeryang Baek 40d5082b9f Merge pull request #6345 from open-webui/leaderboard
feat: leaderboard
2024-10-22 23:45:16 -07:00
Timothy J. Baek 6536c5b7f7 enh: leaderboard percentage 2024-10-22 23:44:13 -07:00
Timothy J. Baek 139136e700 refac: styling 2024-10-22 23:31:51 -07:00
Timothy J. Baek 5101b440a8 refac: styling 2024-10-22 23:28:41 -07:00
Timothy J. Baek 75980010a9 refac: styling 2024-10-22 23:27:07 -07:00
Timothy J. Baek f6893edcc2 feat: leaderboard rating 2024-10-22 23:24:49 -07:00
Timothy J. Baek ce85400817 refac: feedback 2024-10-22 22:55:34 -07:00
Timothy J. Baek bc95e62600 feat: leaderboard 2024-10-22 20:14:10 -07:00
Timothy J. Baek 4c691c0edb refac: rich text input behaviour 2024-10-22 18:34:07 -07:00
Timothy J. Baek 5499b5acc8 enh: arena models toggle tooltip
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2024-10-22 18:28:08 -07:00
Timothy J. Baek 0a765744b8 refac 2024-10-22 18:12:08 -07:00
Timothy J. Baek 00e9362c2c enh: arena model rate to reveal 2024-10-22 18:05:30 -07:00
Timothy J. Baek 6d52f913d2 enh: arena model send selected model id 2024-10-22 17:43:39 -07:00
Timothy J. Baek 2d99f275a3 refac: multi response scroll behaviour 2024-10-22 17:34:53 -07:00
Timothy Jaeryang Baek 170ec2f9d0 Merge pull request #6339 from Cyb4Black/fix-not-rely-on-id-token-for-user-info
fix: get userinfo from endpoint, not only from token
2024-10-22 13:36:30 -07:00
Willnow, Patrick 1b5ac834ef fix: get userinfo from endpoint, not only from token
as was suggested by @alvarolopez in #6262
2024-10-22 21:55:12 +02:00
Timothy Jaeryang Baek c57ef980fb Merge pull request #6337 from open-webui/dependabot/npm_and_yarn/npm_and_yarn-dd9e89fd33
build(deps): bump mermaid from 10.9.1 to 10.9.3 in the npm_and_yarn group across 1 directory
2024-10-22 12:30:31 -07:00
Timothy J. Baek 04b324996c fix 2024-10-22 12:05:48 -07:00
Timothy J. Baek b31d314638 refac 2024-10-22 12:04:45 -07:00
Timothy J. Baek 424062d75f fix: emoji generation 2024-10-22 11:23:38 -07:00
dependabot[bot] 00b2038efb build(deps): bump mermaid in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [mermaid](https://github.com/mermaid-js/mermaid).


Updates `mermaid` from 10.9.1 to 10.9.3
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.9.1...v10.9.3)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-22 18:23:33 +00:00
Timothy J. Baek 24e7ae8767 refac: multi response styling 2024-10-22 03:46:24 -07:00
Timothy J. Baek 8d38af8ddd refac 2024-10-22 03:44:54 -07:00
Timothy J. Baek a0692a434d refac 2024-10-22 03:40:03 -07:00
Timothy J. Baek a89d29db9c fix 2024-10-22 03:38:28 -07:00
Timothy Jaeryang Baek b759f488b4 Merge pull request #6324 from RocketRider/patch-2
feat: Allow URL in citations to be different from the displayed name
2024-10-22 03:27:49 -07:00
Timothy Jaeryang Baek 645657e307 Merge pull request #6327 from aleixdorca/dev
i18n: Update catalan translation.json
2024-10-22 03:27:23 -07:00
Timothy J. Baek 3c8944cb12 refac 2024-10-22 03:22:45 -07:00
Timothy J. Baek 9f285fb2fb feat: arena models 2024-10-22 03:16:48 -07:00
MadsLang 8497db9f43 added Danish language 2024-10-22 11:57:28 +02:00
Aleix Dorca ac09d59a3f Update translation.json 2024-10-22 11:17:06 +02:00
Timothy J. Baek ee16177924 refac: playground styling 2024-10-21 21:50:34 -07:00
Michael Möbius 9153525c84 use url if available as document link 2024-10-22 06:35:01 +02:00
Timothy J. Baek a7aa669038 refac: styling 2024-10-21 21:32:45 -07:00
Michael Möbius 41bd16acbe don't overwrite name, keep url separate 2024-10-22 06:32:08 +02:00
Timothy J. Baek ae44445464 refac: styling 2024-10-21 16:10:29 -07:00
Timothy J. Baek 09f7cdc272 fix 2024-10-21 15:43:15 -07:00
Timothy J. Baek b11c48ac16 refac: playground 2024-10-21 15:37:50 -07:00
Timothy J. Baek e0b7d95429 refac 2024-10-21 15:30:30 -07:00
Timothy J. Baek c03cccfd14 refac: playground 2024-10-21 15:24:59 -07:00
Timothy Jaeryang Baek 39e3afd55d Merge pull request #6309 from OriginalSimon/dev
18n: Update Ukrainian translation
2024-10-21 13:58:59 -07:00
Timothy J. Baek fc71f441c4 refac 2024-10-21 13:45:28 -07:00
Simon 117bdfcb70 Update translation.json 2024-10-21 15:02:14 +02:00
Timothy J. Baek 3dd10f2ca0 refac: playground 2024-10-21 05:09:28 -07:00
Timothy J. Baek e2d4a69750 refac: title generation 2024-10-21 04:24:17 -07:00
Timothy J. Baek 24885a2e38 refac 2024-10-21 04:14:49 -07:00
Timothy J. Baek b4e73c7f19 refac: convert_payload_openai_to_ollama 2024-10-21 04:10:28 -07:00
Timothy J. Baek f99facf383 fix: message input issue 2024-10-21 03:17:30 -07:00
Timothy J. Baek d105be9ca2 refac: styling 2024-10-21 02:02:33 -07:00
Timothy J. Baek e1baa9cc3f refac 2024-10-21 02:00:22 -07:00
Timothy J. Baek ec17bbc867 refac: styling 2024-10-21 01:57:19 -07:00
Timothy J. Baek f1b0d7eb41 refac: styling 2024-10-21 01:54:43 -07:00
Timothy J. Baek fc6dc43a19 refac: tagging behaviour 2024-10-21 01:30:22 -07:00
Timothy J. Baek 8d71323009 refac: model workspace styling 2024-10-21 00:50:08 -07:00
Timothy J. Baek ded22b3204 refac: do not check for empty keys 2024-10-21 00:40:01 -07:00
Timothy J. Baek e3b1b717be refac: styling 2024-10-21 00:35:51 -07:00
Timothy J. Baek 2bb82f258a refac: styling 2024-10-21 00:33:21 -07:00
Timothy J. Baek 6cb41a38a6 enh: option to disable update toast 2024-10-21 00:30:29 -07:00
Timothy J. Baek 47e377967e refac: styling 2024-10-21 00:05:27 -07:00
Timothy J. Baek f27c0f7dcf refac: styling 2024-10-20 23:53:36 -07:00
Timothy J. Baek cd8271eb95 fix 2024-10-20 23:48:28 -07:00
Timothy J. Baek e43e91edd3 refac 2024-10-20 23:45:15 -07:00
Timothy J. Baek 7984980619 feat: s3 support 2024-10-20 23:38:26 -07:00
Timothy J. Baek cb86e09005 feat: s3 support (cherry-picked from #6117)
Co-Authored-By: ssnnow <ssnnow@gmail.com>
2024-10-20 22:53:45 -07:00
Timothy J. Baek 822024f44e refac: artifacts panel behaviour 2024-10-20 22:04:21 -07:00
Timothy J. Baek f02ef01e0c refac: wording 2024-10-20 21:43:19 -07:00
Timothy J. Baek d5c1c2f0a7 enh: faster whisper custom model support 2024-10-20 21:34:36 -07:00
Timothy J. Baek 145a7bbda5 refac: wording 2024-10-20 20:43:10 -07:00
Timothy Jaeryang Baek de1e29eed0 Merge pull request #6109 from JoeyShapiro/dev
fix: disallow empty files
2024-10-20 20:41:56 -07:00
Timothy J. Baek 438b277be0 refac: styling 2024-10-20 19:04:30 -07:00
Timothy J. Baek 2f5c65bd1f refac: styling 2024-10-20 18:54:08 -07:00
Timothy J. Baek 4b357a7b62 refac: styling 2024-10-20 18:49:30 -07:00
Timothy J. Baek 3f598ee5c9 chore: format 2024-10-20 18:42:34 -07:00
Timothy Jaeryang Baek c023afac8c Merge pull request #6238 from Cyb4Black/dev
feat: oauth based role management
2024-10-20 18:40:21 -07:00
Timothy J. Baek 9936583477 chore: format 2024-10-20 18:38:06 -07:00
Timothy Jaeryang Baek 768b7e139c Merge branch 'dev' into dev 2024-10-20 18:37:20 -07:00
Timothy J. Baek b529212a2d refac: styling 2024-10-20 18:35:19 -07:00
Timothy J. Baek b38ed4221a refac: styling 2024-10-20 18:31:24 -07:00
Timothy J. Baek 254c6ca709 refac: tools & functions styling 2024-10-20 18:24:50 -07:00
Timothy J. Baek e378f70f34 refac: tag restoration 2024-10-20 18:04:46 -07:00
Timothy J. Baek d23252b8a9 enh: restore tags from chat import 2024-10-20 18:02:41 -07:00
Timothy J. Baek 30eb43f5b8 fix 2024-10-20 17:50:30 -07:00
Timothy J. Baek 4d46bfe03b refac: file table migration 2024-10-20 17:45:37 -07:00
Timothy J. Baek c5787a2b55 refac: placeholder 2024-10-20 17:44:57 -07:00
Timothy J. Baek 67647c8747 refac 2024-10-20 14:45:47 -07:00
Patrick Willnow d14443a4ba Update Dockerfile
Removed as requested in PR
2024-10-20 14:44:31 +02:00
Timothy J. Baek da9a6c1078 refac: styling 2024-10-20 02:10:15 -07:00
Timothy J. Baek e8591b57b4 enh: allow file download from collection view 2024-10-20 01:58:27 -07:00
Timothy Jaeryang Baek 15cbb66e5e Merge pull request #6292 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-20 01:57:59 -07:00
Timothy J. Baek 05c18cd664 enh: voice input support for knowledge text content 2024-10-20 01:49:44 -07:00
Timothy J. Baek b9b670fcfb refac: styling 2024-10-20 01:24:31 -07:00
Timothy J. Baek a0b58e244e refac: styling 2024-10-20 01:22:20 -07:00
Timothy J. Baek 875e75b8c2 refac 2024-10-20 01:20:27 -07:00
KarlLee830 a850a60a23 i18n: Update Chinese translation 2024-10-20 16:14:15 +08:00
KarlLee830 8102bdbed1 i18n: Update Chinese translation 2024-10-20 16:12:27 +08:00
Timothy J. Baek 97b6fb4766 refac: styling 2024-10-20 00:56:01 -07:00
Timothy J. Baek 146a9d7f05 refac 2024-10-20 00:51:01 -07:00
Timothy J. Baek c5952d62ad refac 2024-10-20 00:47:43 -07:00
Timothy J. Baek a922f4b2e7 refac 2024-10-20 00:42:45 -07:00
Timothy J. Baek 03282da45c refac: collection styling 2024-10-20 00:36:43 -07:00
Timothy J. Baek e8c629a2e2 refac: styling 2024-10-19 23:17:47 -07:00
Timothy J. Baek e530914328 enh: custom tags generation prompt support 2024-10-19 21:27:10 -07:00
Timothy J. Baek 7476bcaa2b enh: filter by untagged chat 2024-10-19 21:16:59 -07:00
Timothy J. Baek 86999157de refac: disable 'none' tag 2024-10-19 21:04:56 -07:00
Timothy J. Baek 54b843c367 refac 2024-10-19 20:58:19 -07:00
Timothy J. Baek 8330fcdb5c refac 2024-10-19 20:47:30 -07:00
Timothy J. Baek d795940ced feat: chat auto tag 2024-10-19 20:34:17 -07:00
Timothy J. Baek 2db0f58dcb refac: user message edit 2024-10-19 18:42:26 -07:00
Timothy J. Baek 3069452210 refac 2024-10-19 17:55:57 -07:00
Timothy J. Baek 7ce51f2b4c refac 2024-10-19 17:49:28 -07:00
Timothy J. Baek 335b6b6c7a refac 2024-10-19 17:29:58 -07:00
Timothy J. Baek 953a8285f7 refac 2024-10-19 16:18:14 -07:00
Timothy J. Baek 73b33c3781 fix: rich text input width styling 2024-10-19 15:03:20 -07:00
Timothy J. Baek 60dd3230ae fix: rich input space behaviour 2024-10-19 14:56:30 -07:00
Timothy J. Baek 3436523b79 fix: input commands 2024-10-19 13:40:20 -07:00
Timothy J. Baek 211c41843c fix: sidebar search 2024-10-19 13:28:27 -07:00
Timothy J. Baek e252ca1dc4 refac: rich text input styling 2024-10-19 03:15:40 -07:00
Timothy J. Baek 6d25d23326 refac: styling 2024-10-19 03:04:12 -07:00
Timothy J. Baek 82edd0e3d9 refac: hide pipelines save button if not present 2024-10-19 02:53:50 -07:00
Timothy J. Baek f6e7af346e refac: unescape rich text input 2024-10-19 02:50:49 -07:00
Timothy J. Baek 7d322a7238 enh: folder export 2024-10-19 02:42:12 -07:00
Timothy J. Baek 3c1afa97af refac: styling 2024-10-19 02:23:10 -07:00
Timothy J. Baek 545f2a0fe2 enh: hide made by community 2024-10-19 02:12:34 -07:00
Timothy J. Baek e153108b51 refac 2024-10-19 01:45:17 -07:00
Timothy J. Baek 971e54d7b4 refac: selector styling 2024-10-19 01:44:45 -07:00
Timothy J. Baek c993cc5515 refac: styling 2024-10-19 01:42:07 -07:00
Timothy J. Baek fb84fc8e77 refac: selector styling 2024-10-19 01:34:03 -07:00
Timothy J. Baek ce917eba81 enh: model url search param 2024-10-19 01:30:49 -07:00
Timothy J. Baek 81f62d5bff refac 2024-10-19 01:07:30 -07:00
Timothy J. Baek 0556479cbe enh: ctrl+shift+delete chat delete support 2024-10-19 01:06:30 -07:00
Timothy J. Baek 99f70cac8b fix: model w/ custom name delete issue 2024-10-19 00:50:00 -07:00
Timothy J. Baek 94c4aa8ddb refac: styling 2024-10-19 00:48:16 -07:00
Timothy J. Baek ca6a624534 refac: styling 2024-10-19 00:45:29 -07:00
Timothy J. Baek 28d6425f87 refac: styling 2024-10-19 00:41:58 -07:00
Timothy J. Baek 6b43cc97d2 chore: format 2024-10-19 00:37:21 -07:00
Timothy J. Baek 6c0d3ce736 refac: tab text variable select 2024-10-19 00:23:59 -07:00
Timothy J. Baek 0107a70343 refac: styling 2024-10-18 23:54:41 -07:00
Timothy J. Baek f46b95300b feat: rich text input for chat 2024-10-18 23:54:35 -07:00
Timothy J. Baek 5e96922eba refac: styling 2024-10-18 22:56:04 -07:00
Timothy J. Baek a3728e6957 refac 2024-10-18 22:40:15 -07:00
Timothy J. Baek 353dc83542 enh: rich text input space behaviour 2024-10-18 22:16:18 -07:00
Timothy Jaeryang Baek 2fc8fa1869 Merge pull request #6274 from open-webui/main
dev
2024-10-18 21:33:54 -07:00
Timothy Jaeryang Baek c2d1a3164e Merge pull request #6261 from monotykamary/main
fix: default to False if stream is unavailable
2024-10-18 21:33:38 -07:00
Timothy J. Baek 79cb9383d9 refac 2024-10-18 21:30:55 -07:00
Timothy J. Baek 670441f548 feat: rich text input 2024-10-18 14:55:39 -07:00
Timothy J. Baek 988a5e2b8d refac: folder/pinned behaviour 2024-10-18 14:18:13 -07:00
Timothy J. Baek 8648a330f2 refac 2024-10-18 14:11:13 -07:00
Timothy J. Baek cad31f6f2b fix 2024-10-18 14:08:43 -07:00
Timothy Jaeryang Baek 8ce5c2eaf0 Merge pull request #6263 from aleixdorca/dev
i18n: Update catalan translation
2024-10-18 14:06:42 -07:00
Aleix Dorca 001788b6e4 Minor Update to translation.json 2024-10-18 12:35:38 +02:00
Aleix Dorca f0fbc586c8 Update catalan translation.json 2024-10-18 12:33:29 +02:00
Tom X Nguyen 11588af34c fix: default to False if stream is unavailable 2024-10-18 11:33:08 +07:00
Timothy J. Baek 2962aa9f06 fix 2024-10-17 20:29:31 -07:00
Timothy J. Baek f821de9470 enh: drag and drop import to folders 2024-10-17 20:13:28 -07:00
Timothy J. Baek 590dc0895f refac 2024-10-17 19:46:37 -07:00
Timothy J. Baek e6f4da2bfc refac 2024-10-17 19:45:18 -07:00
Timothy J. Baek c171e624eb enh: export chat from chat item 2024-10-17 19:30:20 -07:00
Timothy J. Baek b877bc0086 enh: folder delete confirmation
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2024-10-17 19:19:26 -07:00
Timothy J. Baek 351c29917b refac 2024-10-17 19:09:01 -07:00
Timothy J. Baek 6e4820cad8 refac 2024-10-17 18:42:36 -07:00
Timothy J. Baek 7ffa3cb022 refac: folder deletion 2024-10-17 18:24:58 -07:00
Timothy J. Baek 42d048741c fix: disable file item click during upload 2024-10-17 18:15:59 -07:00
Timothy J. Baek dff9254e34 chore: format 2024-10-17 13:30:21 -07:00
Timothy Jaeryang Baek e6fea74b60 Merge pull request #5989 from jannikstdl/dev
enh: citations show relevance score - compact citation view
2024-10-17 13:26:20 -07:00
Timothy Jaeryang Baek c9c79852a5 Merge branch 'dev' into dev 2024-10-17 13:25:39 -07:00
Timothy J. Baek 186923210b refac: styling 2024-10-17 13:17:37 -07:00
Timothy J. Baek 6336d34b59 fix: web attachment issue 2024-10-17 13:08:10 -07:00
Timothy J. Baek f54ab7e551 fix 2024-10-17 00:55:53 -07:00
Timothy J. Baek 4dcbf1af07 enh: share rating to community 2024-10-17 00:51:46 -07:00
Timothy J. Baek 86adcf33b0 refac: styling 2024-10-17 00:33:53 -07:00
Timothy Jaeryang Baek 87d2738864 Merge pull request #6247 from open-webui/folders
feat: folders
2024-10-17 00:18:18 -07:00
Timothy J. Baek 3578c85e39 feat: folder menu 2024-10-17 00:17:50 -07:00
Timothy J. Baek b402061546 enh: open folder on drop 2024-10-16 23:50:06 -07:00
Timothy J. Baek d8b513023c feat: chat folder drag and drop support 2024-10-16 23:45:50 -07:00
Timothy J. Baek 36a541d6b0 enh: drag and drop folders 2024-10-16 23:06:53 -07:00
Timothy J. Baek d5bf32f240 enh: save isExpanded state 2024-10-16 22:58:28 -07:00
Timothy J. Baek e421be5759 refac: ui optimisation 2024-10-16 22:41:16 -07:00
Timothy J. Baek 29c39d44e1 fix: collapsible space toggle issue 2024-10-16 22:36:44 -07:00
Timothy J. Baek 7b97d7a718 enh: disable drag event listener if parent dragged 2024-10-16 22:04:21 -07:00
Timothy J. Baek 9df9f4a990 refac 2024-10-16 21:55:25 -07:00
Timothy J. Baek dea12360f4 refac: folder id -> uuid 2024-10-16 21:49:22 -07:00
Timothy J. Baek a942c30ca8 feat: folder ui 2024-10-16 21:05:03 -07:00
Timothy J. Baek ede71740d2 feat: folders table 2024-10-16 15:13:38 -07:00
Patrick Willnow 8e807bcf8a Merge branch 'open-webui:dev' into dev 2024-10-16 21:43:30 +02:00
Willnow, Patrick ea070e34f9 Remove pending from allowed roles 2024-10-16 21:41:47 +02:00
Timothy Jaeryang Baek de442792ca Merge pull request #6235 from tklk-forks/add-python-dep
chore: add googleapis-common-protos dep
2024-10-16 12:13:21 -07:00
Willnow, Patrick 9ad07ad0ce Add WEBUI_SESSION_COOKIE-settings missing from merge conflict 2024-10-16 20:30:35 +02:00
Willnow, Patrick b888ee17ff Merge branch 'main' into dev
# Conflicts:
#	backend/open_webui/main.py
2024-10-16 20:16:05 +02:00
techknowlogick e6802c7e98 chore: add googleapis-common-protos dep 2024-10-16 13:38:48 -04:00
Willnow, Patrick b1554be3f2 Fix imports 2024-10-16 16:58:03 +02:00
Willnow, Patrick 57d54160d3 Merge branch 'implement-oauth-role-management'
# Conflicts:
#	backend/open_webui/main.py
2024-10-16 16:54:43 +02:00
Willnow, Patrick 8eebd6bce1 Finish reorganizing oauth code 2024-10-16 16:32:57 +02:00
Timothy J. Baek c797c2e18b refac 2024-10-16 03:35:26 -07:00
Timothy J. Baek dedb26fd5c feat: folders db migration 2024-10-16 02:53:16 -07:00
Willnow, Patrick 08ff494754 WIP
- refactoring oauth functions to enable refresh functionality
2024-10-16 09:42:47 +02:00
Timothy J. Baek eef9045dcc chore: format 2024-10-15 09:22:03 -07:00
Timothy Jaeryang Baek 2f2af94d73 Merge pull request #6194 from OriginalSimon/dev
18n: Update Ukrainian translation
2024-10-15 09:17:41 -07:00
Simon 2af9727bd3 Merge branch 'open-webui:dev' into dev 2024-10-15 14:17:30 +02:00
Simon b5f46023d4 Update translation.json 2024-10-15 14:17:09 +02:00
Timothy J. Baek 0131afe667 refac: styling 2024-10-15 05:12:56 -07:00
Timothy J. Baek 69d0472898 refac: styling 2024-10-15 05:00:36 -07:00
Timothy J. Baek 86c961e342 fix 2024-10-15 03:30:02 -07:00
Timothy J. Baek bf0043881a refac 2024-10-15 03:11:03 -07:00
Timothy J. Baek 98ba3f8428 refac: styling 2024-10-15 03:02:56 -07:00
Timothy J. Baek 24f149f686 refac: pinned chat behaviour 2024-10-15 02:52:56 -07:00
Timothy J. Baek b4cd503a02 refac 2024-10-15 02:51:08 -07:00
Timothy J. Baek 97bb9d41a6 refac 2024-10-15 02:48:41 -07:00
Timothy J. Baek 2f4c04055c enh: drag and drop chat to pin 2024-10-15 02:12:39 -07:00
Timothy J. Baek b01f9e8ec3 refac: folder 2024-10-15 00:35:14 -07:00
Timothy J. Baek 73a251fc49 enh: drag ghost 2024-10-14 23:55:50 -07:00
Timothy J. Baek cf24a65caa fix: duplicate tags 2024-10-14 23:04:10 -07:00
Timothy J. Baek 9d2d53bfb5 fix 2024-10-14 22:59:17 -07:00
Timothy J. Baek 6703cacb99 fix: tag unarchive/archive issue 2024-10-14 22:57:11 -07:00
Timothy J. Baek d8a30bd6ae refac: sidebar tag add behaviour 2024-10-14 21:21:45 -07:00
Timothy J. Baek 4c7651c113 refac: pinned collapsible styling 2024-10-14 21:15:36 -07:00
Timothy J. Baek 2afc5f3339 refac 2024-10-14 21:10:04 -07:00
Timothy J. Baek 93dab86e8d refac: dragged overlay behaviour to only activate for files 2024-10-14 21:03:55 -07:00
Timothy J. Baek f8bb77324d refac: styling 2024-10-14 20:36:16 -07:00
Timothy J. Baek 29e8e2d938 refac: rating submit -> save 2024-10-14 20:35:06 -07:00
Timothy J. Baek 90b7754cd6 refac: collapsible pinned chat list 2024-10-14 19:33:32 -07:00
Timothy J. Baek bc75870289 fix: tag search 2024-10-14 19:02:08 -07:00
Timothy J. Baek effa77379e refac 2024-10-14 18:53:29 -07:00
Timothy J. Baek 8eb45acf10 refac 2024-10-14 18:47:41 -07:00
Timothy J. Baek 1db1ef7c26 refac: tag search 2024-10-14 17:31:52 -07:00
Timothy J. Baek 0b2c7046cd fix: archived chats 2024-10-14 15:29:43 -07:00
Timothy J. Baek 466eea1a4c fix: voice note mic not stopping issue 2024-10-14 14:53:01 -07:00
Timothy J. Baek f0179270e2 fix: tag migration 2024-10-14 13:45:09 -07:00
Timothy J. Baek 86bc3023b3 refac: styling 2024-10-14 12:52:20 -07:00
Timothy J. Baek 6f07afc79b refac: styling 2024-10-14 12:46:05 -07:00
Timothy Jaeryang Baek 7a0933a72f Merge pull request #6182 from aleixdorca/dev
i18n: Update catalan translation.json
2024-10-14 12:34:30 -07:00
Aleix Dorca c5921dea58 Update catalan translation.json 2024-10-14 11:38:33 +02:00
Jannik Streidl 79c834d0e4 fix: do not show relevances it you have mixed collections (cosine + l2) 2024-10-14 11:18:13 +02:00
Jannik Streidl 33c3dbd9fa fix 2024-10-14 10:37:54 +02:00
Jannik Streidl f0f4de59eb Merge branch 'upstream-dev' into dev 2024-10-14 09:50:40 +02:00
Timothy Jaeryang Baek 6233494ed1 Merge pull request #6179 from shirayu/fix_typos
fix: Fix typos
2024-10-14 00:29:59 -07:00
Timothy J. Baek dce91a8557 fix: custom model action button issue 2024-10-14 00:28:21 -07:00
Yuta Hayashibe 12516c8a45 fix: Fix typos 2024-10-14 16:22:07 +09:00
Timothy J. Baek 1294ba9d61 refac: styling 2024-10-13 23:53:53 -07:00
Timothy J. Baek ee079df8ed refac: code execution styling 2024-10-13 23:49:32 -07:00
Timothy J. Baek e0e249c1b9 refac: convention 2024-10-13 23:16:51 -07:00
Timothy J. Baek a4a5614de5 refac 2024-10-13 23:07:32 -07:00
Timothy J. Baek 55f14e37ea refac: code_execution.uuid -> id
convention
2024-10-13 23:04:04 -07:00
Timothy Jaeryang Baek adb1bfcaa8 Merge pull request #5955 from EtiennePerot/code-execution-message
feat: add code execution status to chat messages.
2024-10-13 22:58:59 -07:00
Timothy Jaeryang Baek 67885c71dc Merge pull request #6176 from luochen1990/feat-openai-model-list-timeout
add: OPENAI_MODEL_LIST_TIMEOUT
2024-10-13 22:56:59 -07:00
Timothy J. Baek ee8b8220f0 refac: OPENAI_MODEL_LIST_TIMEOUT -> AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST 2024-10-13 22:56:33 -07:00
LuoChen ebec6cd426 add: OPENAI_MODEL_LIST_TIMEOUT 2024-10-14 09:00:09 +08:00
Timothy J. Baek 6f3f5ff922 refac: styling 2024-10-13 04:36:27 -07:00
Timothy Jaeryang Baek 5c45643028 Merge pull request #6160 from shirayu/improve_translation_ja-JP
i18n: Update Japanese translation
2024-10-13 04:29:56 -07:00
Timothy J. Baek 586e005f0f enh: token text splitter support 2024-10-13 04:24:13 -07:00
Yuta Hayashibe 28892fe2d7 i18n: Update Japanese translation 2024-10-13 20:18:06 +09:00
Timothy J. Baek 8a0da6d376 enh: include file name to context 2024-10-13 03:58:51 -07:00
Timothy J. Baek 797afd0b72 fix: embedding metadata issue 2024-10-13 03:25:11 -07:00
Timothy J. Baek 92605fd59f refac 2024-10-13 03:16:18 -07:00
Timothy J. Baek 5ffd216fca refac 2024-10-13 03:02:02 -07:00
Timothy J. Baek dff3732fcd enh: tiktoken/token splitter support 2024-10-13 02:07:50 -07:00
Timothy J. Baek 8ae605ec4b fix: multi-user tags issue 2024-10-13 01:00:38 -07:00
Timothy J. Baek 5273dc4535 refac 2024-10-13 00:21:06 -07:00
Timothy J. Baek 112cbdccbb revert: pdf gen 2024-10-13 00:05:28 -07:00
Timothy J. Baek 5dc05eac67 chore: pyproject bump 2024-10-12 23:07:36 -07:00
Timothy Jaeryang Baek 9c1820f785 Merge pull request #6156 from noczero/add-pdf-generator
Feat: Enhance PDF Downloader to Export Chat
2024-10-12 22:46:50 -07:00
noczero c41261e72b feat: PDF generator to export chat 2024-10-13 11:20:09 +07:00
Timothy J. Baek bb97535cad refac: sentence transformers bump 2024-10-12 18:32:04 -07:00
Timothy J. Baek 333317a7ce refac: add embedding metadata 2024-10-12 18:30:21 -07:00
Etienne Perot 9fbff16a08 feat: add code execution status to chat messages.
This adds `code_executions` as an array of code execution statuses to
chat messages. The intent of this data is to be displayed in a similar
manner as citations: at the bottom of the message, with buttons that open
a modal for more info. However, code execution data doesn't fit well in
citation modals, because they fundamentally differ in their formatting.
Code execution status includes the code that was run (which benefits from
being syntax-highlighted), and the output and generated files. This
differs from citations which are just list of document names and links.

Additionally, code execution is a process, whereas citations are only
emitted once. This is why code execution data uses an ID-based approach,
where each code execution instance is identified by a unique ID and can
be updated by emitting a new `code_execution` message with the same ID.
This allows the code execution status to be updated as code runs.
2024-10-12 16:14:12 -07:00
Jannik Streidl f47c9c69e3 Merge branch 'upstream-dev' into dev 2024-10-12 15:18:59 +02:00
Jannik Streidl 0bebc898c8 finalizing fixes & additions 2024-10-12 15:18:56 +02:00
Jannik Streidl 9d4d96429f only show relevance pertentage score if distances are in cosinus silimarity range 2024-10-12 13:51:27 +02:00
Timothy J. Baek 5c48fce382 fix: async-timeout 2024-10-12 02:42:31 -07:00
Timothy J. Baek e576ddce67 enh: tooltip 2024-10-12 02:37:09 -07:00
Timothy J. Baek c5b670cccd refac: styling 2024-10-12 02:32:15 -07:00
Timothy J. Baek 5a96fcbeaf enh: enable selecting individual files from collection 2024-10-12 02:31:10 -07:00
Timothy J. Baek 0a08495fec refac: styling 2024-10-12 01:27:55 -07:00
Timothy J. Baek 5837dcbfd4 refac: chat controls resize behaviour 2024-10-12 01:10:10 -07:00
Timothy J. Baek 11d7aec61d fix 2024-10-11 16:21:56 -07:00
Timothy J. Baek 30d3d28b9f refac 2024-10-11 15:37:46 -07:00
Timothy J. Baek 1cad157071 fix 2024-10-11 15:29:58 -07:00
Timothy J. Baek 428fd202c5 fix: custom comfyui prompt issue 2024-10-11 13:11:17 -07:00
Timothy Jaeryang Baek db35ea0ae1 Merge pull request #6129 from open-webui/main
refac
2024-10-11 12:11:47 -07:00
Willnow, Patrick 1c5b6987e2 add missing env mapping 2024-10-11 14:08:11 +02:00
Timothy J. Baek 5867ebb1ee refac 2024-10-11 00:28:26 -07:00
Timothy Jaeryang Baek 11afce7895 Merge pull request #6112 from djismgaming/dev
**i18n** Spanish translation update
2024-10-11 00:14:23 -07:00
Timothy J. Baek 63d297756a enh: mermaid copy content button 2024-10-11 00:10:00 -07:00
Timothy J. Baek ba2df1c33a refac 2024-10-11 00:00:13 -07:00
Timothy J. Baek 9658c2559a refac 2024-10-10 23:43:08 -07:00
Timothy J. Baek acb5dcf30a refac: tags 2024-10-10 23:22:53 -07:00
Ismael 7f586f1c3d Added missing new lines on two other translations (non-Spanish) to fix some Format & Build Frontend action issues for them. 2024-10-10 22:25:56 -04:00
Ismael b2c492ce1f Spanish translation update
Updated the new keys added and some minor fixes based on text context.
2024-10-10 21:58:11 -04:00
Timothy Jaeryang Baek 4adc57fd34 Merge pull request #6104 from Peter-De-Ath/hotfix/ollama-embed
fix: ollama embed support list[str] or str on input
2024-10-10 15:10:39 -07:00
Peter De-Ath c748d33192 support list[str] | str as input 2024-10-10 22:41:57 +01:00
Willnow, Patrick edc15d0d7c rewrite oauth role management logic to allow any custom roles to be used for oauth role to open webui role mapping 2024-10-10 23:00:05 +02:00
JoeyShapiro df824db042 fix: disallow empty files 2024-10-10 15:33:27 -05:00
Timothy J. Baek e8babe62bc refac 2024-10-10 12:51:00 -07:00
Jannik Streidl 741230bcdb fix 2024-10-10 17:20:50 +02:00
Jannik Streidl 89c77f05a8 chromadb switch to cosine similarity 2024-10-10 16:46:14 +02:00
Timothy J. Baek ad31bd51fd refac 2024-10-09 12:36:52 -07:00
Timothy Jaeryang Baek 7e253df175 Merge pull request #6054 from jeeteshchel/bugfix/secure-cookie
fix: set token cookie secure and samesite per config
2024-10-09 12:15:19 -07:00
Timothy Jaeryang Baek d7a71f3b34 Merge pull request #6050 from RobinBially/main
feat: add qdrant vector db connector
2024-10-09 12:12:48 -07:00
Timothy Jaeryang Baek 36c9371227 Merge pull request #6062 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-09 12:11:15 -07:00
Timothy J. Baek c157004e07 refac 2024-10-09 12:05:16 -07:00
Timothy J. Baek 451f1bae15 refac: embeddings function 2024-10-09 11:41:35 -07:00
Robin Bially 34150fc3ed add NO_LIMIT var 2024-10-09 18:34:04 +02:00
Robin Bially 54dc94317c improvements 2024-10-09 18:29:14 +02:00
KarlLee830 ce5143c535 i18n: Update Chinese translation 2024-10-09 23:14:14 +08:00
Jeetesh Chellani a2e889c8bb fix: set oauth token secure and samesite per config 2024-10-09 14:50:48 +03:00
Robin Bially 2c59f2dcaf fix critical bug 2024-10-09 13:23:04 +02:00
Robin Bially b56f77ed47 improvements 2024-10-09 13:10:23 +02:00
Robin Bially b185524a8c Merge branch 'open-webui:main' into main 2024-10-09 12:52:13 +02:00
Robin Bially 878a570a2c add qdrant as vector db 2024-10-09 12:51:43 +02:00
Timothy J. Baek b38e2fab32 enh: drag and drop chat exports 2024-10-09 00:13:54 -07:00
Timothy J. Baek 3516eea189 refac 2024-10-08 23:57:36 -07:00
Timothy J. Baek a3f2b7045c refac: dropzone body -> chat-container 2024-10-08 23:55:38 -07:00
Timothy J. Baek 37fdb0ea2e refac: move search to backend 2024-10-08 23:37:37 -07:00
Timothy J. Baek e66619262a refac 2024-10-08 22:34:17 -07:00
Timothy J. Baek d7a00af576 refac: convert chat.chat to json data type 2024-10-08 22:02:48 -07:00
Timothy Jaeryang Baek a04f22d55f Merge pull request #6044 from open-webui/main
dev
2024-10-08 20:30:00 -07:00
Timothy J. Baek caeca9d300 refac: styling 2024-10-08 20:29:33 -07:00
Timothy J. Baek 0da0e12096 refac: file download 2024-10-08 20:16:17 -07:00
Timothy J. Baek d48f54f7b2 enh: show both model id and hash 2024-10-08 19:32:53 -07:00
Timothy J. Baek 3391a855f0 fix 2024-10-08 17:03:42 -07:00
Timothy J. Baek a4870f4c91 fix 2024-10-08 16:35:21 -07:00
Timothy J. Baek 7f37b9340d enh: artifacts, overview back button
Co-Authored-By: Thomas Nordentoft <60035638+nordwestt@users.noreply.github.com>
2024-10-08 15:35:35 -07:00
Timothy Jaeryang Baek 187ea38beb Merge pull request #6020 from PeterDaveHello/Update-locale-zh-TW
i18n: Update and improve zh-TW Traditional Chinese translation
2024-10-08 15:21:20 -07:00
Timothy J. Baek 07a556df55 refac: landing page styling 2024-10-08 13:45:34 -07:00
Timothy J. Baek 0feaf6f649 refac 2024-10-08 13:39:27 -07:00
Timothy J. Baek 595cf191fb chore: rm documents 2024-10-08 13:38:16 -07:00
Timothy J. Baek dcab991d44 fix: model dump issue 2024-10-08 11:05:57 -07:00
Timothy J. Baek 457360dae7 refac 2024-10-07 22:53:36 -07:00
Timothy J. Baek 9bd5567552 enh: 'execute' event type 2024-10-07 22:50:57 -07:00
Timothy Jaeryang Baek d2a1f4a93d Merge pull request #6005 from Kazu-zamasu/dev
Up to date ja-JP translation.
2024-10-07 22:46:27 -07:00
Kazumichi Aoki 69bf610b17 Up to date ja-JP translation. 2024-10-08 14:42:04 +09:00
Timothy Jaeryang Baek 89a29653b8 Merge pull request #6003 from saravanan30erd/dev
add offline_mode and disable version check
2024-10-07 22:18:07 -07:00
Timothy J. Baek 345090c769 chore: i18n 2024-10-07 22:14:37 -07:00
Saravanan Palanisamy 16dd352524 add offline_mode and disable version check 2024-10-08 09:13:49 +04:00
Timothy J. Baek 8c677ff7a1 chore: i18n 2024-10-07 22:13:45 -07:00
Timothy Jaeryang Baek 929e665c5e Merge pull request #5998 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-07 20:14:45 -07:00
Karl Lee 4d45083b5d i18n: Update Chinese translation 2024-10-08 10:39:45 +08:00
Karl Lee 3b88ba8812 i18n: Update Chinese translation 2024-10-08 10:36:39 +08:00
Timothy J. Baek 133ff406d7 i18n: french update
Co-Authored-By: Moguiy <101116631+moblangeois@users.noreply.github.com>
2024-10-07 18:33:14 -07:00
Timothy Jaeryang Baek d05c2f56ea Merge pull request #4715 from Peter-De-Ath/ollama-batch-embeddings
feat: support ollama batch processing embeddings
2024-10-07 18:32:08 -07:00
Timothy J. Baek 3c5b216612 fix 2024-10-07 18:26:03 -07:00
Timothy J. Baek f099b277c8 enh: youtube watch param support 2024-10-07 18:19:13 -07:00
Timothy J. Baek e7ae9a2107 refac 2024-10-07 17:39:06 -07:00
Peter De-Ath b242460874 formatting 2024-10-08 00:04:35 +01:00
Peter De-Ath 3fcdca2f28 backwards comp: use RAG_EMBEDDING_OPENAI_BATCH_SIZE 2024-10-08 00:04:35 +01:00
Peter De-Ath 885b9f1ece refactor: Update GenerateEmbeddingsForm to support batch processing
refactor: Update embedding batch size handling in RAG configuration

refactor: add query_doc query caching

refactor: update logging statements in generate_chat_completion function

change embedding_batch_size to Optional
2024-10-08 00:04:35 +01:00
Timothy J. Baek 09f34a7561 refac 2024-10-07 14:57:56 -07:00
Timothy J. Baek cf544b9e60 refac: styling 2024-10-07 14:51:41 -07:00
Timothy J. Baek 5a78f1d915 refac: collection styling 2024-10-07 14:49:26 -07:00
Timothy J. Baek 854024cdf8 refac: show chat menu in temp chat 2024-10-07 14:28:28 -07:00
Timothy Jaeryang Baek fe872aa3fc Merge pull request #5963 from aleixdorca/dev
i18n: Update catalan translation.json
2024-10-07 14:07:26 -07:00
Timothy J. Baek d9ccef8150 refac 2024-10-07 14:05:45 -07:00
Timothy J. Baek 958d882ff9 refac: knowledge file handling ui behaviour 2024-10-07 14:04:06 -07:00
Timothy J. Baek 48e7f47558 refac 2024-10-07 14:03:42 -07:00
Jannik Streidl 73c291193b fix: do not toggle collapsible if you click on a citation 2024-10-07 21:22:36 +02:00
Jannik Streidl 209948af6f styling 2024-10-07 21:15:10 +02:00
Jannik Streidl 9fc813cfa6 fix: only append if distances are available 2024-10-07 21:13:13 +02:00
Jannik Streidl b105efa05f enh: append citations with distance scores 2024-10-07 21:11:04 +02:00
Jannik Streidl 86caca495b enh: show source documents vector distance + cleaner source view 2024-10-07 21:04:06 +02:00
Peter Dave Hello 4eb47716e2 i18n: Update and improve zh-TW Traditional Chinese translation 2024-10-08 01:51:50 +08:00
Timothy Jaeryang Baek 3f892583c3 Merge pull request #5965 from open-webui/dev
fix: update info toast dismiss issue
2024-10-07 08:42:55 +02:00
Timothy J. Baek ed9fbe153b fix: update info toast dismiss issue 2024-10-06 23:41:50 -07:00
Aleix Dorca d255f5cf8d Update catalan translation.json 2024-10-07 08:20:36 +02:00
Timothy Jaeryang Baek ad7bc624c2 Merge pull request #5961 from open-webui/dev
refac: floating buttons
2024-10-07 07:55:39 +02:00
Timothy J. Baek 6e8e0454e4 refac: floating buttons 2024-10-06 22:55:10 -07:00
Timothy Jaeryang Baek 7cebcf064a Merge pull request #5958 from open-webui/dev
refac: floating buttons behaviour
2024-10-07 07:10:54 +02:00
Timothy J. Baek 1fc1bf5f0d refac: floating buttons behaviour 2024-10-06 22:09:58 -07:00
Timothy Jaeryang Baek bc29d5d3c3 Merge pull request #5956 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.32
2024-10-07 07:06:48 +02:00
Timothy J. Baek fe18aebdd9 doc: changelog 2024-10-06 22:06:24 -07:00
Timothy J. Baek 57df49274c refac: tools url param handling 2024-10-06 22:00:23 -07:00
Timothy J. Baek 9193c9bd7d refac: update info url 2024-10-06 21:52:13 -07:00
Timothy J. Baek 656e75372c enh: workspace models, prompts, tools, functions count 2024-10-06 21:51:03 -07:00
Timothy J. Baek 6f9080dfe0 fix: call mode not working in landing page 2024-10-06 21:42:47 -07:00
Timothy J. Baek 86f822fd9a chore: pyproject bumps 2024-10-06 21:06:47 -07:00
Timothy Jaeryang Baek 8b3972c6df Merge pull request #5954 from open-webui/dev
refac: url param tool_ids -> tools/tool-ids
2024-10-07 05:43:56 +02:00
Timothy J. Baek 58ca5e42c2 refac: url param tool_ids -> tools/tool-ids 2024-10-06 20:41:56 -07:00
Timothy Jaeryang Baek cd62d89dd9 Merge pull request #5953 from open-webui/dev
enh: floating buttons styling
2024-10-07 05:31:44 +02:00
Timothy J. Baek 3686cf7365 enh: esc to close floating buttons 2024-10-06 20:29:30 -07:00
Timothy J. Baek aaa4350eb4 refac: floating buttons styling 2024-10-06 20:26:35 -07:00
Timothy Jaeryang Baek 2113c2f57f Merge pull request #5952 from open-webui/dev
fix: web, youtube attachment issue
2024-10-07 04:44:45 +02:00
Timothy J. Baek ee22ba9676 fix: web, youtube attachment issue 2024-10-06 19:44:02 -07:00
Timothy Jaeryang Baek 466974f344 Merge pull request #5950 from open-webui/dev
refac: styling
2024-10-07 04:09:51 +02:00
Timothy J. Baek 95616e92d7 refac: styling 2024-10-06 19:09:27 -07:00
Timothy Jaeryang Baek c8c41e07e9 Merge pull request #5744 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.31
2024-10-07 03:50:06 +02:00
Timothy J. Baek d195b83c9e doc: changelog 2024-10-06 18:48:56 -07:00
Timothy J. Baek 0bff6d38ee doc: changelog 2024-10-06 18:35:04 -07:00
Timothy J. Baek b0bf6b5d31 doc: changelog 2024-10-06 18:29:57 -07:00
Timothy J. Baek 7abdf8e342 refac: wording 2024-10-06 18:28:41 -07:00
Timothy J. Baek cfb7357103 doc: changelog 2024-10-06 18:27:49 -07:00
Timothy J. Baek 05c15b017d fix: milvus 2024-10-06 17:58:09 -07:00
Timothy J. Baek c8e609c3d1 chore: format 2024-10-06 16:51:07 -07:00
Timothy J. Baek 4623fdfce2 refac: styling 2024-10-06 16:40:27 -07:00
Timothy J. Baek 1c1e6a7172 refac 2024-10-06 16:39:46 -07:00
Timothy J. Baek 297835628a refac 2024-10-06 16:32:44 -07:00
Timothy J. Baek 7b6723a955 refac: styling 2024-10-06 16:22:19 -07:00
Timothy J. Baek a40eb87c31 refac: styling 2024-10-06 16:17:38 -07:00
Timothy J. Baek 53954e4c05 feat: ask floating button 2024-10-06 16:14:12 -07:00
Timothy J. Baek 50b1a3471c refac 2024-10-06 15:30:16 -07:00
Timothy J. Baek dc6e68bd60 refac 2024-10-06 15:27:19 -07:00
Timothy J. Baek da1e88a427 enh: __task_body__ param 2024-10-06 14:56:49 -07:00
Timothy J. Baek d23f05fe0a refac 2024-10-06 14:52:12 -07:00
Timothy J. Baek 90effa925e fix 2024-10-06 13:14:35 -07:00
Timothy J. Baek 8a3c9b9a3e refac 2024-10-06 13:07:41 -07:00
Timothy J. Baek a7f543d737 refac 2024-10-06 13:00:53 -07:00
Timothy J. Baek 4b00036dbf refac: styling 2024-10-06 12:54:20 -07:00
Timothy J. Baek babfc97c90 enh: svg zoom pan 2024-10-06 12:51:29 -07:00
Timothy J. Baek 913620ff0c refac 2024-10-06 12:07:45 -07:00
Timothy J. Baek e6265897c8 refac: styling 2024-10-06 11:54:57 -07:00
Timothy J. Baek 91e1b548a9 refac: styling 2024-10-06 11:52:19 -07:00
Timothy J. Baek 8ad44cd690 enh: chat landing ui option 2024-10-06 11:45:13 -07:00
Timothy J. Baek 5f74cfaa51 enh: artifacts fullscreen 2024-10-06 11:26:02 -07:00
Timothy Jaeryang Baek 9ef3fb0bc7 Merge pull request #5940 from JEleniel/voice_i18n_sort
fix: sort the list of TTS voices and reformat the names
2024-10-06 20:12:47 +02:00
Timothy Jaeryang Baek 7d1890c7d1 Merge pull request #5938 from Nowheresly/fix5930
feat: db pool config #5935
2024-10-06 20:11:50 +02:00
JEleniel eb2aaea6cb fix, i18n - sort the list of TTS voices and reformat the names returned by the built in speech syntehsis 2024-10-06 12:56:30 -04:00
Sylvere Richard 9706b76b36 feat: db pool config #5935 2024-10-06 17:20:10 +02:00
Timothy J. Baek 059ac466e0 refac: styling 2024-10-06 02:49:16 -07:00
Timothy J. Baek 75780256d1 refac: styling 2024-10-06 02:04:41 -07:00
Timothy J. Baek b5efe15f73 refac 2024-10-06 01:58:33 -07:00
Timothy J. Baek be657f45e4 refac: styling 2024-10-06 01:50:06 -07:00
Timothy J. Baek f295e26f25 chore: bump 2024-10-06 01:39:22 -07:00
Timothy J. Baek 0591cd82d4 chore: format 2024-10-06 01:35:58 -07:00
Timothy J. Baek 70ca6527f6 enh: copy artifacts 2024-10-06 01:27:21 -07:00
Timothy J. Baek cdd024db81 refac: close artifacts if html codeblock is not found 2024-10-06 01:20:10 -07:00
Timothy J. Baek 3082d0de7a enh: apply edit to artifacts 2024-10-06 01:12:30 -07:00
Timothy J. Baek f5e10704c0 refac 2024-10-06 00:50:38 -07:00
Timothy J. Baek c90cd6019a refac 2024-10-06 00:46:43 -07:00
Timothy J. Baek c26d7ff463 refac 2024-10-06 00:42:24 -07:00
Timothy J. Baek b21af908a7 refac 2024-10-06 00:33:01 -07:00
Timothy J. Baek 097e90f1de refac 2024-10-06 00:30:50 -07:00
Timothy J. Baek ed47e24494 refac 2024-10-06 00:28:33 -07:00
Timothy J. Baek aeaf761ecd enh: automatic artifacts toggle 2024-10-06 00:14:05 -07:00
Timothy J. Baek 6d2eaaf602 refac 2024-10-06 00:09:37 -07:00
Timothy J. Baek 1815cfe99d refac 2024-10-06 00:03:58 -07:00
Timothy J. Baek 539e95a206 refac 2024-10-06 00:00:39 -07:00
Timothy J. Baek de59ecf8a3 feat: artifacts
Co-Authored-By: Andrew Tait Gehrhardt <134739775+atgehrhardt@users.noreply.github.com>
2024-10-05 23:58:02 -07:00
Timothy J. Baek 2981212dfa refac 2024-10-05 23:55:44 -07:00
Timothy J. Baek 7ee07df26a refac 2024-10-05 22:23:06 -07:00
Timothy J. Baek 008febb6d7 refac: styling 2024-10-05 22:18:05 -07:00
Timothy J. Baek 6417ccef00 chore: format 2024-10-05 21:29:08 -07:00
Timothy J. Baek 198749aed1 refac: styling 2024-10-05 21:27:47 -07:00
Timothy J. Baek 46bd97c100 refac: styling 2024-10-05 21:25:04 -07:00
Timothy J. Baek 1086a0e662 refac: styling 2024-10-05 21:16:20 -07:00
Timothy J. Baek 9586749314 refac: styling 2024-10-05 21:14:35 -07:00
Timothy J. Baek 54552c3d13 refac 2024-10-05 21:02:37 -07:00
Timothy J. Baek ed337b94e1 refac: styling 2024-10-05 20:50:58 -07:00
Timothy J. Baek 0f287c8a09 enh: floating codeblock buttons 2024-10-05 20:48:55 -07:00
Timothy J. Baek dc2e75017d refac 2024-10-05 20:16:46 -07:00
Timothy J. Baek c3862bc387 refac: code rendering 2024-10-05 19:58:27 -07:00
Timothy J. Baek 6fdfa62845 refac 2024-10-05 19:53:19 -07:00
Timothy J. Baek e0fc3e89a7 refac: i18n 2024-10-05 19:42:04 -07:00
Timothy Jaeryang Baek ed5669410b Merge pull request #5927 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-06 04:38:24 +02:00
Timothy Jaeryang Baek 6d3d80b347 Merge pull request #5926 from Nowheresly/useNode22
Update to node 22
2024-10-06 04:37:42 +02:00
Karl Lee d84368a8f6 i18n: Update Chinese translation 2024-10-06 10:29:43 +08:00
Timothy J. Baek 5adf9ed445 refac 2024-10-05 19:03:39 -07:00
Timothy J. Baek 49e9275c5b refac: styling 2024-10-05 18:57:19 -07:00
Timothy J. Baek c746fd94cb refac 2024-10-05 18:43:17 -07:00
Timothy J. Baek 0bfe28711a enh: dark theme scrollbar 2024-10-05 18:28:40 -07:00
Timothy J. Baek c09e51c1bf fix: banner styling 2024-10-05 17:09:49 -07:00
Timothy J. Baek 3dd1d1bc4a fix 2024-10-05 17:05:26 -07:00
Timothy J. Baek a860e98ab9 refac: remember sidebar status 2024-10-05 17:00:56 -07:00
Timothy J. Baek ed1a2ab5e8 refac: explain button behaviour 2024-10-05 16:50:06 -07:00
Timothy J. Baek bbbd94f69c refac 2024-10-05 16:16:51 -07:00
Timothy J. Baek 2e067b0541 refac 2024-10-05 16:00:25 -07:00
Timothy J. Baek a0a79fbc5b refac: styling 2024-10-05 15:55:30 -07:00
Timothy J. Baek ecd6a135cd refac: styling 2024-10-05 15:47:51 -07:00
Timothy J. Baek 9c18cf204d refac 2024-10-05 15:10:55 -07:00
Timothy J. Baek a571ae4ec2 refac: only show resizer when controls is open 2024-10-05 14:54:15 -07:00
Timothy J. Baek 1558f64c48 refac: only show floating buttons when message.done 2024-10-05 14:51:28 -07:00
Timothy J. Baek 1739313c13 refac 2024-10-05 14:49:39 -07:00
Timothy J. Baek 1b8acc42b4 refac 2024-10-05 14:27:16 -07:00
Timothy J. Baek 1397bfa84c refac: navbar styling 2024-10-05 14:26:54 -07:00
Sylvere Richard c09af435ac WIP node 22
https://github.com/cypress-io/github-action/pull/1189
2024-10-05 22:39:24 +02:00
Timothy J. Baek f5004fd9d4 refac: styling 2024-10-05 13:26:52 -07:00
Timothy J. Baek fb58e842b7 refac: styling 2024-10-05 13:25:30 -07:00
Timothy Jaeryang Baek dcf8d9c7eb Merge pull request #5923 from Nowheresly/fixIntegTests
fix: fix cypress test on CI
2024-10-05 21:46:02 +02:00
Timothy Jaeryang Baek a61c54531c Merge pull request #5925 from Nowheresly/pythonActionUpd
Update to actions/setup-python@v5
2024-10-05 21:45:43 +02:00
Timothy J. Baek e184a65dea refac 2024-10-05 12:43:58 -07:00
Sylvere Richard fafeec3d86 Update to actions/setup-python@v5 2024-10-05 21:42:01 +02:00
Timothy J. Baek 55d0ecc879 refac 2024-10-05 12:41:18 -07:00
Timothy J. Baek 614f3d5f80 fix 2024-10-05 12:40:13 -07:00
Timothy J. Baek 46328333c7 refac 2024-10-05 12:38:44 -07:00
Sylvere Richard 582036fb3f fix: fix cypress test on CI 2024-10-05 21:38:20 +02:00
Timothy J. Baek 28602b12ea refac: dynamic lang support 2024-10-05 12:38:09 -07:00
Timothy J. Baek f5b6785e53 refac 2024-10-05 12:07:45 -07:00
Timothy J. Baek 81440460f2 feat: editable code block 2024-10-05 12:04:36 -07:00
Timothy Jaeryang Baek 82ac2e4edb Merge pull request #5920 from Nowheresly/baseImgDockerUpd
fix: ensure Dockerfile and github actions use the same nodejs version
2024-10-05 19:54:27 +02:00
Sylvere Richard fc44924256 fix: ensure Dockerfile and github actions use the same nodejs version 2024-10-05 19:48:56 +02:00
Timothy J. Baek 9ca2350a13 fix: call tts issue 2024-10-05 10:42:06 -07:00
Timothy J. Baek 12a6216477 refac: styling 2024-10-05 10:23:47 -07:00
Timothy J. Baek fb5fb27ebe fix 2024-10-05 10:20:11 -07:00
Timothy J. Baek cb0f759420 refac: knowledge collection uploading indicator 2024-10-05 10:18:43 -07:00
Timothy J. Baek 378223aedb refac: comments 2024-10-05 10:08:48 -07:00
Timothy J. Baek 61b147441c refac 2024-10-05 10:05:12 -07:00
Timothy J. Baek 1f9b5b6456 refac: retain metadata for collection 2024-10-05 09:58:46 -07:00
Timothy Jaeryang Baek 4ca870bf6d Merge pull request #5919 from Nowheresly/dockerwarn
refac: remove docker warnings during image build
2024-10-05 18:33:11 +02:00
Sylvere Richard 52a3ab5333 refac: remove docker warnings during image build 2024-10-05 12:30:47 +02:00
Timothy J. Baek ff22e2156d refac: styling 2024-10-05 03:29:55 -07:00
Timothy J. Baek e0c775f6e3 refac 2024-10-05 03:24:00 -07:00
Timothy J. Baek 7036166535 refac 2024-10-05 03:22:17 -07:00
Timothy J. Baek 2e231982a3 refac 2024-10-05 03:21:22 -07:00
Timothy J. Baek fe7e7abf15 refac 2024-10-05 03:19:55 -07:00
Timothy J. Baek 3e7a163660 refac: styling 2024-10-05 03:15:39 -07:00
Timothy J. Baek 0319e63999 enh: new landing page 2024-10-05 03:07:56 -07:00
Timothy J. Baek 0bd88090bb refac: DOCS_DIR deprecated 2024-10-05 01:45:22 -07:00
Timothy J. Baek ed3e1397ca refac 2024-10-05 01:40:10 -07:00
Timothy J. Baek 0ad35ffad9 feat: text select quick actions 2024-10-05 01:37:39 -07:00
Timothy J. Baek 1f488a0072 enh: sync confirm 2024-10-04 19:38:24 -07:00
Timothy J. Baek 493745a70b refac 2024-10-04 19:32:33 -07:00
Timothy J. Baek c400f40663 enh: knowledge search in model editor 2024-10-04 19:24:36 -07:00
Timothy J. Baek bc6113f4ba refac 2024-10-04 19:10:52 -07:00
Timothy J. Baek b6703be859 chore: format 2024-10-04 19:00:48 -07:00
Timothy Jaeryang Baek 62f30ce098 Merge pull request #5917 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-05 03:59:17 +02:00
Timothy J. Baek 9f812e7022 enh: support non chrome browsers 2024-10-04 18:54:40 -07:00
Timothy J. Baek a909aa1c20 enh: sync directory 2024-10-04 18:44:57 -07:00
Timothy J. Baek e3889522d6 refac 2024-10-04 18:28:48 -07:00
Timothy J. Baek 79c005a041 refac: deprecate docs_dir 2024-10-04 18:22:55 -07:00
Timothy J. Baek a6c797d4c2 refac: process docs dir 2024-10-04 17:22:00 -07:00
Karl Lee 0e50c8bb31 i18n: Update Chinese translation 2024-10-05 08:12:14 +08:00
Timothy J. Baek 9ad5ffb8c1 chore: endpoint naming 2024-10-04 17:03:56 -07:00
Timothy J. Baek f052010d8e refac 2024-10-04 16:58:45 -07:00
Timothy J. Baek ee6e41b144 refac: legacy support 2024-10-04 16:56:20 -07:00
Timothy J. Baek 2e267b420a fix: model knowledge 2024-10-04 16:53:25 -07:00
Timothy J. Baek 0c618b8145 refac: legacy detection 2024-10-04 16:43:41 -07:00
Timothy J. Baek 8f41db2f2e refac: styling 2024-10-04 16:43:25 -07:00
Timothy J. Baek a53537ccde fix: disable empty fields 2024-10-04 16:18:44 -07:00
Timothy J. Baek 5017ca90ff refac 2024-10-04 16:16:16 -07:00
Timothy J. Baek f3ee07a8a2 enh: knowledge collection search 2024-10-04 12:35:06 -07:00
Timothy Jaeryang Baek 263cc71dd3 Merge pull request #5913 from calvarez8922/fr-translation
i18n: fix fr year translation on date
2024-10-04 21:02:39 +02:00
Timothy J. Baek 8f5e426a13 enh: important note 2024-10-04 11:26:06 -07:00
Timothy Jaeryang Baek 87700d02a7 Merge pull request #5911 from aleixdorca/dev
i18n: Updated Catalan Translation
2024-10-04 20:18:47 +02:00
Timothy J. Baek 8f22e911e0 fix: empty knowledge delete issue 2024-10-04 11:11:53 -07:00
Aleix Dorca 1647cbef21 Update catalan translation.json 2024-10-04 15:06:06 +02:00
Aleix Dorca 528ac51ba9 Update translation.json 2024-10-04 15:00:49 +02:00
Willnow, Patrick f751d22a20 Refinement 2024-10-04 13:26:49 +02:00
Timothy J. Baek 650ca95784 fix 2024-10-04 01:21:33 -07:00
Patrick Willnow 6ddd8c7241 fix logic 2024-10-04 10:14:20 +02:00
Timothy J. Baek d1b8af6220 chore: format 2024-10-04 01:04:04 -07:00
Timothy Jaeryang Baek 332ef045ee Merge pull request #5870 from KarlLee830/translate
i18n: Update Chinese translation
2024-10-04 10:01:56 +02:00
Timothy Jaeryang Baek 0876c9b5ef Merge pull request #5829 from jannikstdl/query-embedding-perf-fix
fix:  performance issues on large collections
2024-10-04 10:01:17 +02:00
Timothy Jaeryang Baek ebc7da6f82 Merge pull request #5861 from open-webui/projects
feat: knowledge/projects
2024-10-04 10:00:47 +02:00
Timothy J. Baek 1fe1c27220 refac: legacy support 2024-10-04 00:59:19 -07:00
Timothy J. Baek 8013c152d0 feat: edit file content support 2024-10-04 00:46:32 -07:00
Timothy J. Baek 630a78cead fix 2024-10-04 00:24:51 -07:00
Timothy J. Baek 17c772831d refac 2024-10-04 00:23:14 -07:00
Timothy J. Baek 6147f41589 refac: styling 2024-10-03 23:45:20 -07:00
Timothy J. Baek 5e70afc054 refac: styling 2024-10-03 23:43:41 -07:00
Timothy J. Baek 75fae69def refac: styling 2024-10-03 23:41:17 -07:00
Timothy J. Baek 90ef9f751a fix 2024-10-03 23:32:20 -07:00
Timothy J. Baek 544251a7fd feat: add text content 2024-10-03 23:24:44 -07:00
Timothy J. Baek 05970157f6 refac 2024-10-03 23:06:47 -07:00
Timothy J. Baek d834bd2a18 refac 2024-10-03 22:35:43 -07:00
Timothy J. Baek e9b68524e8 refac 2024-10-03 22:24:05 -07:00
Timothy J. Baek b291271df3 refac 2024-10-03 22:22:22 -07:00
Timothy J. Baek 9dd76b72b4 refac: styling 2024-10-03 21:34:53 -07:00
Timothy J. Baek 12977e07f3 feat: collection file content editor 2024-10-03 21:31:42 -07:00
Timothy J. Baek 29467a7057 fix 2024-10-03 21:12:35 -07:00
Timothy J. Baek b862dff185 refac 2024-10-03 21:10:33 -07:00
Timothy J. Baek 6747478f67 refac 2024-10-03 21:05:55 -07:00
Timothy J. Baek 124a17e826 refac 2024-10-03 20:58:56 -07:00
Timothy J. Baek 57360b7a61 refac 2024-10-03 20:51:21 -07:00
jose.c.alvarez.diaz 2d40efe9d6 bug: fix fr year translation 2024-10-03 20:27:30 -06:00
Willnow, Patrick 79b9c8a677 handling no claim received when nested expected 2024-10-04 00:05:36 +02:00
Willnow, Patrick 8e4776ada1 add handling nested claims... 2024-10-03 23:25:00 +02:00
Willnow, Patrick 5b2e1ca7cd add more logging 2024-10-03 23:06:05 +02:00
Willnow, Patrick 0a7373dae1 add pending as role fallback
add logging to determine correct handling of oauth roles
2024-10-03 22:56:52 +02:00
Willnow, Patrick c9d948f284 Remove copy pasta error of calling value on bool 2024-10-03 21:38:56 +02:00
Willnow, Patrick dc92178641 Fix missing key mapping 2024-10-03 20:55:32 +02:00
Timothy J. Baek 2fc07fd6a2 enh: vector db hash collision check 2024-10-03 06:53:21 -07:00
Timothy J. Baek 78413d0c2e enh: add/remove file from knowledge 2024-10-03 06:46:20 -07:00
Timothy J. Baek 1c01e52f7c refac: fileItem styling 2024-10-03 06:45:29 -07:00
Timothy J. Baek 15a3c6b171 refac 2024-10-03 06:45:15 -07:00
Timothy J. Baek d394f8b7be enh: add to vector db support 2024-10-03 06:44:17 -07:00
Timothy J. Baek 325ca98773 enh: vector db delete filter support 2024-10-03 06:43:50 -07:00
Patrick Willnow 9a691c0387 Add oauth role mapping
also add node env to allow local build to succeed
2024-10-03 11:12:14 +02:00
Timothy J. Baek 351b1dbf31 refac 2024-10-02 21:14:58 -07:00
Timothy J. Baek a2eadb30f5 refac 2024-10-02 20:42:10 -07:00
Timothy Jaeryang Baek 2e7e346e19 Merge pull request #5871 from tcgumus/dev
fix: Unsupported JSON schema type {type_}
2024-10-03 05:07:30 +02:00
Timothy Jaeryang Baek 7c145ce5d5 Merge pull request #5879 from cheahjs/feat/arbitrary-usage-info
feat: show the user the entirety of the usage response
2024-10-03 05:06:56 +02:00
Timothy Jaeryang Baek f19b61307d Merge pull request #5878 from cheahjs/feat/lazy-load-new-imports-for-mem
perf: lazy load big dependencies to reduce min memory usage
2024-10-03 00:15:03 +02:00
Jun Siang Cheah a8ec73d01e feat: show the user the entirety of the usage response 2024-10-02 23:14:08 +01:00
Jun Siang Cheah 318f61161e refac: lazy load big dependencies to reduce min memory usage 2024-10-02 22:18:42 +01:00
Timothy J. Baek 1a26e67611 refac: styling 2024-10-02 07:44:07 -07:00
Timothy J. Baek 6974cb248c refac 2024-10-02 07:37:01 -07:00
Timothy J. Baek dd5da10d2a feat: knowledge page 2024-10-02 07:35:43 -07:00
Timothy J. Baek 9c4b55c86a refac: knowledge fuzzy search 2024-10-02 06:21:16 -07:00
Timothy J. Baek 4aca1e86ad refac 2024-10-02 06:19:09 -07:00
Tuna Çağlar Gümüş b2f09e4623 Merge pull request #1 from tcgumus/dev-fix-unsupported-json-error
fix: Unsupported JSON schema type {type_}
2024-10-02 15:40:22 +03:00
t bdb2ce9448 fix: Unsupported JSON schema type {type_} 2024-10-02 15:34:48 +03:00
Karl Lee 973c08babd i18n: Update Chinese translation 2024-10-02 17:51:45 +08:00
Timothy J. Baek 7f51ef1838 refac 2024-10-01 23:21:33 -07:00
Timothy J. Baek 08969ecf89 refac: rename projects -> knowledge 2024-10-01 22:45:04 -07:00
Timothy J. Baek 3e012f0219 refac 2024-10-01 21:48:15 -07:00
Timothy J. Baek 5933d7a216 enh: sort by descending order 2024-10-01 21:35:18 -07:00
Timothy J. Baek 1b7d363d32 refac 2024-10-01 21:32:59 -07:00
Timothy J. Baek c2732a0990 refac 2024-10-01 17:46:56 -07:00
Timothy J. Baek c5eb0a9732 refac: documents -> projects 2024-10-01 17:35:35 -07:00
Timothy J. Baek bf57dd808e feat: project migration 2024-10-01 17:35:10 -07:00
Timothy Jaeryang Baek d353ea449a Merge pull request #5864 from sebdanielsson/remove-bun
build: Remove bun
2024-10-02 00:42:50 +02:00
Sebastian c85cdbde46 Delete bun.lockb 2024-10-02 00:00:46 +02:00
Timothy J. Baek fb083237cd refac 2024-10-01 14:00:19 -07:00
Timothy J. Baek a0fb4a9b84 refac 2024-10-01 13:13:39 -07:00
Timothy J. Baek 5c9dd25459 refac: files migration 2024-10-01 11:01:26 -07:00
Timothy J. Baek 0907c32e10 refac 2024-10-01 09:51:58 -07:00
Timothy J. Baek 4752df9bd8 refac 2024-10-01 09:40:08 -07:00
Timothy J. Baek 7b9b29253f Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-10-01 09:18:15 -07:00
Timothy Jaeryang Baek 126aff7a7d Merge pull request #5839 from open-webui/dependabot/pip/backend/dev/langchain-chroma-0.1.4
chore(deps): bump langchain-chroma from 0.1.2 to 0.1.4 in /backend
2024-10-01 17:58:14 +02:00
Timothy Jaeryang Baek 6ca1de3601 Merge pull request #5838 from open-webui/dependabot/pip/backend/dev/duckduckgo-search-approx-eq-6.2.13
chore(deps): update duckduckgo-search requirement from ~=6.2.11 to ~=6.2.13 in /backend
2024-10-01 17:57:59 +02:00
Timothy Jaeryang Baek fb8af637e1 Merge pull request #5837 from open-webui/dependabot/pip/backend/dev/aiohttp-3.10.8
chore(deps): bump aiohttp from 3.10.5 to 3.10.8 in /backend
2024-10-01 17:57:51 +02:00
Timothy Jaeryang Baek ab983887cc Merge pull request #5836 from open-webui/dependabot/pip/backend/dev/pandas-2.2.3
chore(deps): bump pandas from 2.2.2 to 2.2.3 in /backend
2024-10-01 17:57:40 +02:00
Timothy Jaeryang Baek 75c1f47500 Merge pull request #5835 from open-webui/dependabot/pip/backend/dev/pydantic-2.9.2
chore(deps): bump pydantic from 2.8.2 to 2.9.2 in /backend
2024-10-01 17:57:29 +02:00
Timothy Jaeryang Baek 3da981d9cd Merge pull request #5831 from aindriu80/main
Added Irish language support
2024-10-01 17:57:11 +02:00
Timothy Jaeryang Baek 8a0e30a901 Merge pull request #5830 from FINNSEEFLY/task/adjust-ru-localization
i18n: Update ru-RU localization
2024-10-01 17:56:46 +02:00
dependabot[bot] 736cbb961f chore(deps): bump langchain-chroma from 0.1.2 to 0.1.4 in /backend
Bumps [langchain-chroma](https://github.com/langchain-ai/langchain) from 0.1.2 to 0.1.4.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-chroma==0.1.2...langchain-chroma==0.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 02:22:25 +00:00
dependabot[bot] 8d8c2752dc chore(deps): update duckduckgo-search requirement in /backend
Updates the requirements on [duckduckgo-search](https://github.com/deedy5/duckduckgo_search) to permit the latest version.
- [Release notes](https://github.com/deedy5/duckduckgo_search/releases)
- [Commits](https://github.com/deedy5/duckduckgo_search/compare/v6.2.12...v6.2.13)

---
updated-dependencies:
- dependency-name: duckduckgo-search
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 02:22:21 +00:00
dependabot[bot] d78a7d431d chore(deps): bump aiohttp from 3.10.5 to 3.10.8 in /backend
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.8.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.5...v3.10.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 02:22:18 +00:00
dependabot[bot] 91ca5c424b chore(deps): bump pandas from 2.2.2 to 2.2.3 in /backend
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.2.2...v2.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 02:22:08 +00:00
dependabot[bot] 7e1c55d2ed chore(deps): bump pydantic from 2.8.2 to 2.9.2 in /backend
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.8.2 to 2.9.2.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.8.2...v2.9.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 02:22:04 +00:00
Aindriú Mac Giolla Eoin 3a2c241450 Added Irish language support 2024-09-30 17:47:01 +01:00
FINNSEEFLY 13b6b43cea Adjust ru-RU localization 2024-09-30 18:36:02 +03:00
Timothy J. Baek c7a0e45bea refac 2024-09-30 16:32:38 +02:00
Jannik Streidl 6bff5a4d09 fix: do not embed the query every single time 2024-09-30 16:18:02 +02:00
Timothy J. Baek 37e0d47082 refac 2024-09-30 13:13:19 +02:00
Timothy J. Baek e6b91036e1 refac 2024-09-30 13:12:41 +02:00
Timothy J. Baek 79a83adc89 refac 2024-09-30 13:03:47 +02:00
Timothy J. Baek ffd598c5d7 enh: summary tag support 2024-09-30 12:50:53 +02:00
Timothy Jaeryang Baek d255251e5f Merge pull request #5813 from res0Nanz/patch-1
feat: add `apple-touch-icon`
2024-09-30 12:17:06 +02:00
Eman Lear 49fe04a627 frontend: add apple-touch-icon
With `apple-touch-icon`, mobile devices of particular OS can properly show icon when bookmarking the web page as a home screen application.
2024-09-30 15:20:57 +08:00
Timothy J. Baek 21c919988d refac 2024-09-30 01:01:39 +02:00
Timothy J. Baek 1c4b6b9cd9 refac 2024-09-30 01:00:13 +02:00
Timothy J. Baek 3899405864 refac 2024-09-30 00:39:30 +02:00
Timothy J. Baek 209828c7c3 refac: styling 2024-09-30 00:37:31 +02:00
Timothy J. Baek 7152af949b feat: compress audio
Co-Authored-By: Beck Bekmyradov <47065940+bekmuradov@users.noreply.github.com>
2024-09-30 00:30:12 +02:00
Timothy J. Baek 8206c47a47 refac 2024-09-29 23:20:37 +02:00
Timothy J. Baek f7aba20d79 refac 2024-09-29 23:11:22 +02:00
Timothy J. Baek 6afc686e17 refac 2024-09-29 23:08:55 +02:00
Timothy J. Baek 677c36c3aa refac 2024-09-29 22:55:53 +02:00
Timothy J. Baek 6d764ee55e feat: retrieval whole document mode 2024-09-29 22:52:27 +02:00
Timothy J. Baek 1d8b3b8c51 refac 2024-09-29 22:11:50 +02:00
Timothy J. Baek 92dd173b27 refac 2024-09-29 18:55:26 +02:00
Timothy J. Baek f2ec020b64 refac: styling 2024-09-29 18:41:27 +02:00
Timothy J. Baek d784d5c367 refac 2024-09-29 18:33:16 +02:00
Timothy J. Baek b3517c63e8 fix: multi model chat infinite loop issue 2024-09-29 18:29:50 +02:00
Timothy J. Baek 550075bba4 fix: action button not working 2024-09-29 18:24:44 +02:00
Timothy J. Baek c93a10388b refac 2024-09-28 19:51:28 +02:00
Timothy J. Baek 5a168ecc2a refac 2024-09-28 19:25:41 +02:00
Timothy J. Baek 276ce3374d refac 2024-09-28 19:17:21 +02:00
Timothy J. Baek e77c3ab043 refac 2024-09-28 19:16:52 +02:00
Timothy J. Baek d2e2e535dd refac 2024-09-28 10:55:52 +02:00
Timothy J. Baek 90ec458c4c enh: show extracted file content 2024-09-28 10:53:25 +02:00
Timothy Jaeryang Baek 9636913de0 Merge pull request #5777 from smonux/dev
Fix: sanitize function calling json
2024-09-28 10:07:52 +02:00
smonux e039b4ec54 Merge branch 'open-webui:dev' into dev 2024-09-28 06:01:26 +02:00
Timothy J. Baek 9d2ed3d2be refac 2024-09-28 02:56:56 +02:00
Timothy J. Baek b8b994a820 refac 2024-09-28 02:49:18 +02:00
Timothy J. Baek 00eb022450 refac 2024-09-28 02:38:59 +02:00
Timothy J. Baek 2428878f42 refac 2024-09-28 02:29:08 +02:00
Timothy J. Baek af57a2c153 refac 2024-09-28 02:23:09 +02:00
Timothy J. Baek 1b349016ff refac 2024-09-28 01:36:35 +02:00
Timothy J. Baek c1b4fbf5c2 refac 2024-09-28 01:35:52 +02:00
Timothy J. Baek a52e8cd537 refac 2024-09-28 01:35:31 +02:00
Timothy J. Baek 5b7cf88915 refac 2024-09-28 01:28:45 +02:00
Timothy J. Baek e1103305f5 refac: "rag" endpoints renamed to "retrieval" 2024-09-28 01:27:46 +02:00
Timothy Jaeryang Baek 6e9db3e3c8 Merge pull request #5773 from HaldiH/main
fix: Chat completion 401 when no Authorization header
2024-09-27 21:34:58 +02:00
Hugo Haldi eab30781e0 Chat completion 401 when no Authorization header
When we send a request to `/api/chat/completions` without the `Authorization` header, the server just crashes and creates a stack trace, returning "Internal Server Error" to the calling client. With this fix, the server sends a 401 to the client with the content `{"detail": "Not authenticated"}`.
2024-09-27 20:04:45 +02:00
Timothy Jaeryang Baek c30c876659 Merge pull request #5758 from kivvi3412/fix_o1_system_message
Fix: O1 does not support the system parameter
2024-09-27 19:44:16 +02:00
Timothy J. Baek 4ead3c5b80 chore: format 2024-09-27 19:43:40 +02:00
Timothy Jaeryang Baek f0f176b80f Merge pull request #5765 from aleixdorca/dev
Update catalan translation.json
2024-09-27 19:42:18 +02:00
kivvi 5b3ee30ca9 Merge remote-tracking branch 'origin/fix_o1_system_message' into fix_o1_system_message
# Conflicts:
#	backend/open_webui/apps/openai/main.py
2024-09-27 22:48:36 +08:00
kivvi e13614e11b Fix: O1 does not support the system parameter 2024-09-27 22:47:24 +08:00
Aleix Dorca 464b6a329e Update catalan translation.json 2024-09-27 16:11:52 +02:00
Timothy J. Baek 44d768ecf3 refac: do not wait for update check 2024-09-27 14:41:29 +02:00
Timothy J. Baek 0bd9d59c78 refac: update check timeout 2024-09-27 14:38:56 +02:00
kivvi be74a4c9c1 Fix: O1 does not support the system parameter 2024-09-27 20:18:13 +08:00
Timothy J. Baek 719f4da1dc fix: milvus collection creation issue 2024-09-26 22:59:09 +02:00
Timothy Jaeryang Baek fbc9634ffc Merge pull request #5742 from not-a-ethan/corrected-link
FIX: Migration link updated
2024-09-26 22:41:04 +02:00
Ethan 619c81472b Migration link updated 2024-09-26 16:27:49 -04:00
smonux d8f71e1d7f Some models produce almost correct json during function calling, but with additional data before of after it. This solves it. 2024-09-26 22:02:56 +02:00
Timothy J. Baek 1715446b13 fix: call mode persisting after width change issue 2024-09-26 21:45:19 +02:00
Timothy Jaeryang Baek 8204f06485 Merge pull request #5741 from open-webui/main
dev
2024-09-26 21:34:16 +02:00
Timothy J. Baek 4c92a0f571 chore: chromadb, pymilvus bump 2024-09-26 21:33:37 +02:00
Timothy J. Baek 1d225dd804 fix: chat pdf, txt export issue 2024-09-26 20:59:25 +02:00
Timothy J. Baek 15bd5ebd7b Update README.md
Co-Authored-By: Aam Surganda <74174455+aamsur-933@users.noreply.github.com>
2024-09-26 15:57:43 +02:00
Timothy Jaeryang Baek 3af50f08bd Merge pull request #5728 from sp301415/dev
fix: Fix OpenAI batch embedding
2024-09-26 14:08:11 +02:00
Hwang In Tak 4fe1f2487d fix: Fix OpenAI batch embedding 2024-09-26 20:48:14 +09:00
Hwang In Tak a4bc0b2829 fix: Fix OpenAI batch embedding 2024-09-26 20:39:40 +09:00
Timothy Jaeryang Baek 7b8f923981 Merge pull request #5711 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.30
2024-09-26 04:13:54 +02:00
Timothy J. Baek be41994d40 Update CHANGELOG.md 2024-09-26 04:12:19 +02:00
Timothy J. Baek 5361896411 doc: changelog 2024-09-26 04:11:13 +02:00
Timothy J. Baek c754c53906 refac 2024-09-26 04:05:28 +02:00
Timothy J. Baek 78d6647885 refac 2024-09-26 03:25:44 +02:00
Timothy J. Baek 4eeb669ac3 refac 2024-09-26 03:21:37 +02:00
Timothy J. Baek 26465f3e92 chore: bump 2024-09-26 03:14:06 +02:00
Timothy J. Baek 9d25207b83 refac 2024-09-26 03:13:38 +02:00
Timothy Jaeryang Baek cc19b8049a Merge pull request #5710 from kivvi3412/fix_o1_max_tokens
Fix: o1 input parameter must be max_completion_tokens
2024-09-26 03:03:15 +02:00
Timothy J. Baek 3a163b6392 fix: pip install database save issue 2024-09-26 03:02:08 +02:00
kivvi 405d0561df Fix: o1 input parameter must be max_completion_tokens 2024-09-26 08:49:59 +08:00
Timothy J. Baek ee33b4e2a3 fix: ollama /embed form_data 2024-09-25 22:34:02 +02:00
Timothy J. Baek 888479aaf0 refac: update toast dismiss behaviour 2024-09-25 20:47:04 +02:00
Timothy Jaeryang Baek 82cda6e522 Merge pull request #5699 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.29
2024-09-25 15:46:39 +02:00
Timothy J. Baek 119a7f1933 doc: changelog 2024-09-25 15:45:36 +02:00
Timothy J. Baek 6c4445d545 fix: filter outlet issue 2024-09-25 13:44:06 +02:00
Timothy J. Baek 92b1acd6fb fix: RESET_CONFIG_ON_START not working 2024-09-25 01:06:11 +02:00
Timothy J. Baek 1767b64135 fix: 'call' url search param not working 2024-09-25 01:02:48 +02:00
Timothy Jaeryang Baek e030f261d1 Merge pull request #5675 from sp301415/dev
fix: Fix KaTeX Rendering (Followup)
2024-09-24 20:58:22 +02:00
Hwang In Tak d501ece247 fix: Fix KaTeX corner cases 2024-09-25 02:15:53 +09:00
Timothy Jaeryang Baek 534e4c90ca Merge pull request #5674 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.28
2024-09-24 18:52:23 +02:00
Timothy J. Baek 40f2c3521b doc: changelog 2024-09-24 18:50:47 +02:00
Timothy J. Baek 07b1327708 fix 2024-09-24 18:32:14 +02:00
Timothy J. Baek 525095b3de fix: websearch not working issue
#5672
2024-09-24 18:25:21 +02:00
Timothy Jaeryang Baek f84513e856 Merge pull request #5673 from open-webui/dev
refac: readme
2024-09-24 18:19:09 +02:00
Timothy J. Baek bf423b8577 refac: readme 2024-09-24 18:18:32 +02:00
Timothy Jaeryang Baek ba20c71963 Merge pull request #5661 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.27
2024-09-24 18:13:08 +02:00
Timothy J. Baek 7bbc57f225 doc: changelog 2024-09-24 18:12:47 +02:00
Timothy J. Baek e703e172e2 chore: format 2024-09-24 18:10:14 +02:00
Timothy Jaeryang Baek 3ff52fd1ad Merge pull request #5655 from sp301415/dev
fix: Fix KaTeX Rendering
2024-09-24 17:44:08 +02:00
Timothy J. Baek e19406cdd7 fix 2024-09-24 17:43:43 +02:00
Hwang In Tak 30e65b33f6 fix: Add comments 2024-09-25 00:41:08 +09:00
Hwang In Tak 3f1255b39e fix: Change inline and block delimiters 2024-09-25 00:10:49 +09:00
Timothy J. Baek 71743b25fe chore: format 2024-09-24 16:42:42 +02:00
Timothy Jaeryang Baek c7e93b32c5 Merge pull request #5660 from open-webui/pypi-release
fix: open-webui serve
2024-09-24 16:36:37 +02:00
Timothy J. Baek 3cee507687 fix: dev2 2024-09-24 16:19:24 +02:00
Timothy J. Baek ff651ddc36 fix: dev1 2024-09-24 16:07:49 +02:00
Timothy Jaeryang Baek c0738cef26 Merge pull request #5659 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.26
2024-09-24 15:41:42 +02:00
Timothy J. Baek a44e9a8dda refac 2024-09-24 15:41:23 +02:00
Timothy J. Baek 38b9a63fa5 refac 2024-09-24 15:40:37 +02:00
Timothy J. Baek 2d60e42258 doc: changelog 2024-09-24 15:40:01 +02:00
Timothy Jaeryang Baek 60ac69eb27 Merge pull request #5656 from OriginalSimon/dev
i18n: Update Ukrainian translation
2024-09-24 15:33:15 +02:00
Timothy J. Baek 504d910557 fix: no running event loop issue 2024-09-24 15:31:55 +02:00
Hwang In Tak e48d66f918 fix: Remove unnecessary logging 2024-09-24 22:11:05 +09:00
Hwang In Tak 0bfbace9aa fix: Simplify regex 2024-09-24 22:00:01 +09:00
Simon f382a78e31 Update translation.json 2024-09-24 13:47:51 +02:00
Hwang In Tak 377cc427b6 fix: Remove unnecessary logging 2024-09-24 20:40:50 +09:00
Hwang In Tak 214546399a fix: fix katex rendering 2024-09-24 16:58:15 +09:00
437 changed files with 60538 additions and 22237 deletions
+2 -1
View File
@@ -16,4 +16,5 @@ _old
uploads
.ipynb_checkpoints
**/*.db
_test
_test
backend/data/*
+14
View File
@@ -8,6 +8,20 @@ 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 youre 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. Were here to help if youre 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, its not that the issue doesnt 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.]
+16
View File
@@ -6,6 +6,22 @@ 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 youre 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. Were here to help if youre 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, its not that the issue doesnt 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 [...]
@@ -28,6 +28,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
- name: Remove git history
run: rm -rf .git
@@ -52,7 +54,9 @@ jobs:
- name: Set up Git and push to Space
run: |
git init --initial-branch=main
git lfs install
git lfs track "*.ttf"
git lfs track "*.jpg"
rm demo.gif
git add .
git commit -m "GitHub deploy: ${{ github.sha }}"
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Or specify any other version you want to use
node-version: '22' # Or specify any other version you want to use
- name: Install Dependencies
run: npm install
@@ -48,7 +48,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
- name: Install Dependencies
run: npm ci
+4 -1
View File
@@ -85,7 +85,7 @@ jobs:
# - uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v4
# uses: actions/setup-python@v5
# with:
# python-version: ${{ matrix.python-version }}
@@ -182,6 +182,9 @@ jobs:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
DATABASE_POOL_SIZE: 10
DATABASE_POOL_MAX_OVERFLOW: 10
DATABASE_POOL_TIMEOUT: 30
run: |
cd backend
uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' &
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Use Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
- name: Use Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
+291
View File
@@ -5,6 +5,297 @@ 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.4.6] - 2024-11-26
### Added
- **🌍 Enhanced Translations**: Various language translations improved to make the WebUI more accessible and user-friendly worldwide.
### Fixed
- **✏️ Textarea Shifting Bug**: Resolved the issue where the textarea shifted unexpectedly, ensuring a smoother typing experience.
- **⚙️ Model Configuration Modal**: Fixed the issue where the models configuration modal introduced in 0.4.5 wasnt working for some users.
- **🔍 Legacy Query Support**: Restored functionality for custom query generation in RAG when using legacy prompts, ensuring both default and custom templates now work seamlessly.
- **⚡ Improved General Reliability**: Various minor fixes improve platform stability and ensure a smoother overall experience across workflows.
## [0.4.5] - 2024-11-26
### Added
- **🎨 Model Order/Defaults Reintroduced**: Brought back the ability to set model order and default models, now configurable via Admin Settings > Models > Configure (Gear Icon).
### Fixed
- **🔍 Query Generation Issue**: Resolved an error in web search query generation, enhancing search accuracy and ensuring smoother search workflows.
- **📏 Textarea Auto Height Bug**: Fixed a layout issue where textarea input height was shifting unpredictably, particularly when editing system prompts.
- **🔑 Ollama Authentication**: Corrected an issue with Ollamas authorization headers, guaranteeing reliable authentication across all endpoints.
- **⚙️ Missing Min_P Save**: Resolved an issue where the 'min_p' parameter was not being saved in configurations.
- **🛠️ Tools Description**: Fixed a key issue that omitted tool descriptions in tools payload.
## [0.4.4] - 2024-11-22
### Added
- **🌐 Translation Updates**: Refreshed Catalan, Brazilian Portuguese, German, and Ukrainian translations, further enhancing the platform's accessibility and improving the experience for international users.
### Fixed
- **📱 Mobile Controls Visibility**: Resolved an issue where the controls button was not displaying on the new chats page for mobile users, ensuring smoother navigation and functionality on smaller screens.
- **📷 LDAP Profile Image Issue**: Fixed an LDAP integration bug related to profile images, ensuring seamless authentication and a reliable login experience for users.
- **⏳ RAG Query Generation Issue**: Addressed a significant problem where RAG query generation occurred unnecessarily without attached files, drastically improving speed and reducing delays during chat completions.
### Changed
- **⚙️ Legacy Event Emitter Support**: Reintroduced compatibility with legacy "citation" types for event emitters in tools and functions, providing smoother workflows and broader tool support for users.
## [0.4.3] - 2024-11-21
### Added
- **📚 Inline Citations for RAG Results**: Get seamless inline citations for Retrieval-Augmented Generation (RAG) responses using the default RAG prompt. Note: This feature only supports newly uploaded files, improving traceability and providing source clarity.
- **🎨 Better Rich Text Input Support**: Enjoy smoother and more reliable rich text formatting for chats, enhancing communication quality.
- **⚡ Faster Model Retrieval**: Implemented caching optimizations for faster model loading, providing a noticeable speed boost across workflows. Further improvements are on the way!
### Fixed
- **🔗 Pipelines Feature Restored**: Resolved a critical issue that previously prevented Pipelines from functioning, ensuring seamless workflows.
- **✏️ Missing Suffix Field in Ollama Form**: Added the missing "suffix" field to the Ollama generate form, enhancing customization options.
### Changed
- **🗂️ Renamed "Citations" to "Sources"**: Improved clarity and consistency by renaming the "citations" field to "sources" in messages.
## [0.4.2] - 2024-11-20
### Fixed
- **📁 Knowledge Files Visibility Issue**: Resolved the bug preventing individual files in knowledge collections from displaying when referenced with '#'.
- **🔗 OpenAI Endpoint Prefix**: Fixed the issue where certain OpenAI connections that deviate from the official API spec werent working correctly with prefixes.
- **⚔️ Arena Model Access Control**: Corrected an issue where arena model access control settings were not being saved.
- **🔧 Usage Capability Selector**: Fixed the broken usage capabilities selector in the model editor.
## [0.4.1] - 2024-11-19
### Added
- **📊 Enhanced Feedback System**: Introduced a detailed 1-10 rating scale for feedback alongside thumbs up/down, preparing for more precise model fine-tuning and improving feedback quality.
- **️ Tool Descriptions on Hover**: Easily access tool descriptions by hovering over the message input, providing a smoother workflow with more context when utilizing tools.
### Fixed
- **🗑️ Graceful Handling of Deleted Users**: Resolved an issue where deleted users caused workspace items (models, knowledge, prompts, tools) to fail, ensuring reliable workspace loading.
- **🔑 API Key Creation**: Fixed an issue preventing users from creating new API keys, restoring secure and seamless API management.
- **🔗 HTTPS Proxy Fix**: Corrected HTTPS proxy issues affecting the '/api/v1/models/' endpoint, ensuring smoother, uninterrupted model management.
## [0.4.0] - 2024-11-19
### Added
- **👥 User Groups**: You can now create and manage user groups, making user organization seamless.
- **🔐 Group-Based Access Control**: Set granular access to models, knowledge, prompts, and tools based on user groups, allowing for more controlled and secure environments.
- **🛠️ Group-Based User Permissions**: Easily manage workspace permissions. Grant users the ability to upload files, delete, edit, or create temporary chats, as well as define their ability to create models, knowledge, prompts, and tools.
- **🔑 LDAP Support**: Newly introduced LDAP authentication adds robust security and scalability to user management.
- **🌐 Enhanced OpenAI-Compatible Connections**: Added prefix ID support to avoid model ID clashes, with explicit model ID support for APIs lacking '/models' endpoint support, ensuring smooth operation with custom setups.
- **🔐 Ollama API Key Support**: Now manage credentials for Ollama when set behind proxies, including the option to utilize prefix ID for proper distinction across multiple Ollama instances.
- **🔄 Connection Enable/Disable Toggle**: Easily enable or disable individual OpenAI and Ollama connections as needed.
- **🎨 Redesigned Model Workspace**: Freshly redesigned to improve usability for managing models across users and groups.
- **🎨 Redesigned Prompt Workspace**: A fresh UI to conveniently organize and manage prompts.
- **🧩 Sorted Functions Workspace**: Functions are now automatically categorized by type (Action, Filter, Pipe), streamlining management.
- **💻 Redesigned Collaborative Workspace**: Enhanced support for multiple users contributing to models, knowledge, prompts, or tools, improving collaboration.
- **🔧 Auto-Selected Tools in Model Editor**: Tools enabled through the model editor are now automatically selected, whereas previously it only gave users the option to enable the tool, reducing manual steps and enhancing efficiency.
- **🔔 Web Search & Tools Indicator**: A clear indication now shows when web search or tools are active, reducing confusion.
- **🔑 Toggle API Key Auth**: Tighten security by easily enabling or disabling API key authentication option for Open WebUI.
- **🗂️ Agentic Retrieval**: Improve RAG accuracy via smart pre-processing of chat history to determine the best queries before retrieval.
- **📁 Large Text as File Option**: Optionally convert large pasted text into a file upload, keeping the chat interface cleaner.
- **🗂️ Toggle Citations for Models**: Ability to disable citations has been introduced in the model editor.
- **🔍 User Settings Search**: Quickly search for settings fields, improving ease of use and navigation.
- **🗣️ Experimental SpeechT5 TTS**: Local SpeechT5 support added for improved text-to-speech capabilities.
- **🔄 Unified Reset for Models**: A one-click option has been introduced to reset and remove all models from the Admin Settings.
- **🛠️ Initial Setup Wizard**: The setup process now explicitly informs users that they are creating an admin account during the first-time setup, ensuring clarity. Previously, users encountered the login page right away without this distinction.
- **🌐 Enhanced Translations**: Several language translations, including Ukrainian, Norwegian, and Brazilian Portuguese, were refined for better localization.
### Fixed
- **🎥 YouTube Video Attachments**: Fixed issues preventing proper loading and attachment of YouTube videos as files.
- **🔄 Shared Chat Update**: Corrected issues where shared chats were not updating, improving collaboration consistency.
- **🔍 DuckDuckGo Rate Limit Fix**: Addressed issues with DuckDuckGo search integration, enhancing search stability and performance when operating within rate limits.
- **🧾 Citations Relevance Fix**: Adjusted the relevance percentage calculation for citations, so that Open WebUI properly reflect the accuracy of a retrieved document in RAG, ensuring users get clearer insights into sources.
- **🔑 Jina Search API Key Requirement**: Added the option to input an API key for Jina Search, ensuring smooth functionality as keys are now mandatory.
### Changed
- **🛠️ Functions Moved to Admin Panel**: As Functions operate as advanced plugins, they are now accessible from the Admin Panel instead of the workspace.
- **🛠️ Manage Ollama Connections**: The "Models" section in Admin Settings has been relocated to Admin Settings > "Connections" > Ollama Connections. You can now manage Ollama instances via a dedicated "Manage Ollama" modal from "Connections", streamlining the setup and configuration of Ollama models.
- **📊 Base Models in Admin Settings**: Admins can now find all base models, both connections or functions, in the "Models" Admin setting. Global model accessibility can be enabled or disabled here. Models are private by default, requiring explicit permission assignment for user access.
- **📌 Sticky Model Selection for New Chats**: The model chosen from a previous chat now persists when creating a new chat. If you click "New Chat" again from the new chat page, it will revert to your default model.
- **🎨 Design Refactoring**: Overall design refinements across the platform have been made, providing a more cohesive and polished user experience.
### Removed
- **📂 Model List Reordering**: Temporarily removed and will be reintroduced in upcoming user group settings improvements.
- **⚙️ Default Model Setting**: Removed the ability to set a default model for users, will be reintroduced with user group settings in the future.
## [0.3.35] - 2024-10-26
### Added
- **🌐 Translation Update**: Added translation labels in the SearchInput and CreateCollection components and updated Brazilian Portuguese translation (pt-BR)
- **📁 Robust File Handling**: Enhanced file input handling for chat. If the content extraction fails or is empty, users will now receive a clear warning, preventing silent failures and ensuring you always know what's happening with your uploads.
- **🌍 New Language Support**: Introduced Hungarian translations and updated French translations, expanding the platform's language accessibility for a more global user base.
### Fixed
- **📚 Knowledge Base Loading Issue**: Resolved a critical bug where the Knowledge Base was not loading, ensuring smooth access to your stored documents and improving information retrieval in RAG-enhanced workflows.
- **🛠️ Tool Parameters Issue**: Fixed an error where tools were not functioning correctly when required parameters were missing, ensuring reliable tool performance and more efficient task completions.
- **🔗 Merged Response Loss in Multi-Model Chats**: Addressed an issue where responses in multi-model chat workflows were being deleted after follow-up queries, improving consistency and ensuring smoother interactions across models.
## [0.3.34] - 2024-10-26
### Added
- **🔧 Feedback Export Enhancements**: Feedback history data can now be exported to JSON, allowing for seamless integration in RLHF processing and further analysis.
- **🗂️ Embedding Model Lazy Loading**: Search functionality for leaderboard reranking is now more efficient, as embedding models are lazy-loaded only when needed, optimizing performance.
- **🎨 Rich Text Input Toggle**: Users can now switch back to legacy textarea input for chat if they prefer simpler text input, though rich text is still the default until deprecation.
- **🛠️ Improved Tool Calling Mechanism**: Enhanced method for parsing and calling tools, improving the reliability and robustness of tool function calls.
- **🌐 Globalization Enhancements**: Updates to internationalization (i18n) support, further refining multi-language compatibility and accuracy.
### Fixed
- **🖥️ Folder Rename Fix for Firefox**: Addressed a persistent issue where users could not rename folders by pressing enter in Firefox, now ensuring seamless folder management across browsers.
- **🔠 Tiktoken Model Text Splitter Issue**: Resolved an issue where the tiktoken text splitter wasnt working in Docker installations, restoring full functionality for tokenized text editing.
- **💼 S3 File Upload Issue**: Fixed a problem affecting S3 file uploads, ensuring smooth operations for those who store files on cloud storage.
- **🔒 Strict-Transport-Security Crash**: Resolved a crash when setting the Strict-Transport-Security (HSTS) header, improving stability and security enhancements.
- **🚫 OIDC Boolean Access Fix**: Addressed an issue with boolean values not being accessed correctly during OIDC logins, ensuring login reliability.
- **⚙️ Rich Text Paste Behavior**: Refined paste behavior in rich text input to make it smoother and more intuitive when pasting various content types.
- **🔨 Model Exclusion for Arena Fix**: Corrected the filter function that was not properly excluding models from the arena, improving model management.
- **🏷️ "Tags Generation Prompt" Fix**: Addressed an issue preventing custom "tags generation prompts" from registering properly, ensuring custom prompt work seamlessly.
## [0.3.33] - 2024-10-24
### Added
- **🏆 Evaluation Leaderboard**: Easily track your performance through a new leaderboard system where your ratings contribute to a real-time ranking based on the Elo system. Sibling responses (regenerations, many model chats) are required for your ratings to count in the leaderboard. Additionally, you can opt-in to share your feedback history and be part of the community-wide leaderboard. Expect further improvements as we refine the algorithm—help us build the best community leaderboard!
- **⚔️ Arena Model Evaluation**: Enable blind A/B testing of models directly from Admin Settings > Evaluation for a true side-by-side comparison. Ideal for pinpointing the best model for your needs.
- **🎯 Topic-Based Leaderboard**: Discover more accurate rankings with experimental topic-based reranking, which adjusts leaderboard standings based on tag similarity in feedback. Get more relevant insights based on specific topics!
- **📁 Folders Support for Chats**: Organize your chats better by grouping them into folders. Drag and drop chats between folders and export them seamlessly for easy sharing or analysis.
- **📤 Easy Chat Import via Drag & Drop**: Save time by simply dragging and dropping chat exports (JSON) directly onto the sidebar to import them into your workspace—streamlined, efficient, and intuitive!
- **📚 Enhanced Knowledge Collection**: Now, you can reference individual files from a knowledge collection—ideal for more precise Retrieval-Augmented Generations (RAG) queries and document analysis.
- **🏷️ Enhanced Tagging System**: Tags now take up less space! Utilize the new 'tag:' query system to manage, search, and organize your conversations more effectively without cluttering the interface.
- **🧠 Auto-Tagging for Chats**: Your conversations are now automatically tagged for improved organization, mirroring the efficiency of auto-generated titles.
- **🔍 Backend Chat Query System**: Chat filtering has become more efficient, now handled through the backend\*\* instead of your browser, improving search performance and accuracy.
- **🎮 Revamped Playground**: Experience a refreshed and optimized Playground for smoother testing, tweaks, and experimentation of your models and tools.
- **🧩 Token-Based Text Splitter**: Introducing token-based text splitting (tiktoken), giving you more precise control over how text is processed. Previously, only character-based splitting was available.
- **🔢 Ollama Batch Embeddings**: Leverage new batch embedding support for improved efficiency and performance with Ollama embedding models.
- **🔍 Enhanced Add Text Content Modal**: Enjoy a cleaner, more intuitive workflow for adding and curating knowledge content with an upgraded input modal from our Knowledge workspace.
- **🖋️ Rich Text Input for Chats**: Make your chat inputs more dynamic with support for rich text formatting. Your conversations just got a lot more polished and professional.
- **⚡ Faster Whisper Model Configurability**: Customize your local faster whisper model directly from the WebUI.
- **☁️ Experimental S3 Support**: Enable stateless WebUI instances with S3 support, greatly enhancing scalability and balancing heavy workloads.
- **🔕 Disable Update Toast**: Now you can streamline your workspace even further—choose to disable update notifications for a more focused experience.
- **🌟 RAG Citation Relevance Percentage**: Easily assess citation accuracy with the addition of relevance percentages in RAG results.
- **⚙️ Mermaid Copy Button**: Mermaid diagrams now come with a handy copy button, simplifying the extraction and use of diagram contents directly in your workflow.
- **🎨 UI Redesign**: Major interface redesign that will make navigation smoother, keep your focus where it matters, and ensure a modern look.
### Fixed
- **🎙️ Voice Note Mic Stopping Issue**: Fixed the issue where the microphone stayed active after ending a voice note recording, ensuring your audio workflow runs smoothly.
### Removed
- **👋 Goodbye Sidebar Tags**: Sidebar tag clutter is gone. Weve shifted tag buttons to more effective query-based tag filtering for a sleeker, more agile interface.
## [0.3.32] - 2024-10-06
### Added
- **🔢 Workspace Enhancements**: Added a display count for models, prompts, tools, and functions in the workspace, providing a clear overview and easier management.
### Fixed
- **🖥️ Web and YouTube Attachment Fix**: Resolved an issue where attaching web links and YouTube videos was malfunctioning, ensuring seamless integration and display within chats.
- **📞 Call Mode Activation on Landing Page**: Fixed a bug where call mode was not operational from the landing page.
### Changed
- **🔄 URL Parameter Refinement**: Updated the 'tool_ids' URL parameter to 'tools' or 'tool-ids' for more intuitive and consistent user experience.
- **🎨 Floating Buttons Styling Update**: Refactored the styling of floating buttons to intelligently adjust to the left side when there isn't enough room on the right, improving interface usability and aesthetic.
- **🔧 Enhanced Accessibility for Floating Buttons**: Implemented the ability to close floating buttons with the 'Esc' key, making workflow smoother and more efficient for users navigating via keyboard.
- **🖇️ Updated Information URL**: Information URLs now direct users to a general release page rather than a version-specific URL, ensuring access to the latest and relevant details all in one place.
- **📦 Library Dependencies Update**: Upgraded dependencies to ensure compatibility and performance optimization for pip installs.
## [0.3.31] - 2024-10-06
### Added
- **📚 Knowledge Feature**: Reimagined documents feature, now more performant with a better UI for enhanced organization; includes streamlined API integration for Retrieval-Augmented Generation (RAG). Detailed documentation forthcoming: https://docs.openwebui.com/
- **🌐 New Landing Page**: Freshly designed landing page; toggle between the new UI and the classic chat UI from Settings > Interface for a personalized experience.
- **📁 Full Document Retrieval Mode**: Toggle between full document retrieval or traditional snippets by clicking on the file item. This mode enhances document capabilities and supports comprehensive tasks like summarization by utilizing the entire content instead of RAG.
- **📄 Extracted File Content Display**: View extracted content directly by clicking on the file item, simplifying file analysis.
- **🎨 Artifacts Feature**: Render web content and SVGs directly in the interface, supporting quick iterations and live changes.
- **🖊️ Editable Code Blocks**: Supercharged code blocks now allow live editing directly in the LLM response, with live reloads supported by artifacts.
- **🔧 Code Block Enhancements**: Introduced a floating copy button in code blocks to facilitate easier code copying without scrolling.
- **🔍 SVG Pan/Zoom**: Enhanced interaction with SVG images, including Mermaid diagrams, via new pan and zoom capabilities.
- **🔍 Text Select Quick Actions**: New floating buttons appear when text is highlighted in LLM responses, offering deeper interactions like "Ask a Question" or "Explain".
- **🗃️ Database Pool Configuration**: Enhanced database handling to support scalable user growth.
- **🔊 Experimental Audio Compression**: Compress audio files to navigate around the 25MB limit for OpenAI's speech-to-text processing.
- **🔍 Query Embedding**: Adjusted embedding behavior to enhance system performance by not repeating query embedding.
- **💾 Lazy Load Optimizations**: Implemented lazy loading of large dependencies to minimize initial memory usage, boosting performance.
- **🍏 Apple Touch Icon Support**: Optimizes the display of icons for web bookmarks on Apple mobile devices.
- **🔽 Expandable Content Markdown Support**: Introducing 'details', 'summary' tag support for creating expandable content sections in markdown, facilitating cleaner, organized documentation and interactive content display.
### Fixed
- **🔘 Action Button Issue**: Resolved a bug where action buttons were not functioning, enhancing UI reliability.
- **🔄 Multi-Model Chat Loop**: Fixed an infinite loop issue in multi-model chat environments, ensuring smoother chat operations.
- **📄 Chat PDF/TXT Export Issue**: Resolved problems with exporting chat logs to PDF and TXT formats.
- **🔊 Call to Text-to-Speech Issues**: Rectified problems with text-to-speech functions to improve audio interactions.
### Changed
- **⚙️ Endpoint Renaming**: Renamed 'rag' endpoints to 'retrieval' for clearer function description.
- **🎨 Styling and Interface Updates**: Multiple refinements across the platform to enhance visual appeal and user interaction.
### Removed
- **🗑️ Deprecated 'DOCS_DIR'**: Removed the outdated 'docs_dir' variable in favor of more direct file management solutions, with direct file directory syncing and API uploads for a more integrated experience.
## [0.3.30] - 2024-09-26
### Fixed
- **🍞 Update Available Toast Dismissal**: Enhanced user experience by ensuring that once the update available notification is dismissed, it won't reappear for 24 hours.
- **📋 Ollama /embed Form Data**: Adjusted the integration inaccuracies in the /embed form data to ensure it perfectly matches with Ollama's specifications.
- **🔧 O1 Max Completion Tokens Issue**: Resolved compatibility issues with OpenAI's o1 models max_completion_tokens param to ensure smooth operation.
- **🔄 Pip Install Database Issue**: Fixed a critical issue where database changes during pip installations were reverting and not saving chat logs, now ensuring data persistence and reliability in chat operations.
- **🏷️ Chat Rename Tab Update**: Fixed the functionality to change the web browser's tab title simultaneously when a chat is renamed, keeping tab titles consistent.
## [0.3.29] - 2023-09-25
### Fixed
- **🔧 KaTeX Rendering Improvement**: Resolved specific corner cases in KaTeX rendering to enhance the display of complex mathematical notation.
- **📞 'Call' URL Parameter Fix**: Corrected functionality for 'call' URL search parameter ensuring reliable activation of voice calls through URL triggers.
- **🔄 Configuration Reset Fix**: Fixed the RESET_CONFIG_ON_START to ensure settings revert to default correctly upon each startup, improving reliability in configuration management.
- **🌍 Filter Outlet Hook Fix**: Addressed issues in the filter outlet hook, ensuring all filter functions operate as intended.
## [0.3.28] - 2024-09-24
### Fixed
- **🔍 Web Search Functionality**: Corrected an issue where the web search option was not functioning properly.
## [0.3.27] - 2024-09-24
### Fixed
- **🔄 Periodic Cleanup Error Resolved**: Fixed a critical RuntimeError related to the 'periodic_usage_pool_cleanup' coroutine, ensuring smooth and efficient performance post-pip install, correcting a persisting issue from version 0.3.26.
- **📊 Enhanced LaTeX Rendering**: Improved rendering for LaTeX content, enhancing clarity and visual presentation in documents and mathematical models.
## [0.3.26] - 2024-09-24
### Fixed
- **🔄 Event Loop Error Resolution**: Addressed a critical error where a missing running event loop caused 'periodic_usage_pool_cleanup' to fail with pip installs. This fix ensures smoother and more reliable updates and installations, enhancing overall system stability.
## [0.3.25] - 2024-09-24
### Fixed
+60 -38
View File
@@ -2,76 +2,98 @@
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community 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, socio-economic 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 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, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
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.
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.**
This is a community where **respect and professionalism are mandatory.** Violations of these standards will result in **zero tolerance** and immediate enforcement to prevent disruption and ensure the well-being of all participants.
## Our Standards
Examples of behavior that contribute to a positive environment for our community include:
Examples of behavior that contribute to a positive and professional community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
- **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences.
- **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism.
- **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience.
- **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas.
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- **Spamming of any kind**
- Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
- Other conduct which could reasonably be considered inappropriate in a professional setting
- The use of discriminatory, demeaning, or sexualized language or behavior.
- Personal attacks, derogatory comments, trolling, or inflammatory political or ideological arguments.
- Harassment, intimidation, or any behavior intended to create a hostile, uncomfortable, or unsafe environment.
- Publishing others' private information (e.g., physical or email addresses) without explicit permission.
- **Entitlement, demand, or aggression toward contributors.** Volunteers are under no obligation to provide immediate or personalized support. Rude or dismissive behavior will not be tolerated.
- **Unproductive or destructive behavior.** This includes venting frustration as hostility ("tantrums"), hypercriticism, attention-seeking negativity, or anything that distracts from the project's goals.
- **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion.
### Feedback and Community Engagement
- **Constructive feedback is encouraged, but hostile or entitled behavior will result in immediate action.** If you disagree with elements of the project, we encourage you to offer meaningful improvements or fork the project if necessary. Healthy discussions and technical disagreements are welcome only when handled with professionalism.
- **Respect contributors' time and efforts.** No one is entitled to personalized or on-demand assistance. This is a community built on collaboration and shared effort; demanding or demeaning behavior undermines that trust and will not be allowed.
### Zero Tolerance: No Warnings, Immediate Action
This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.**
We employ this approach to ensure that unproductive or disruptive behavior does not escalate further or cause unnecessary harm to other contributors. The standards are clear, and violations of any kind—whether mild or severe—will be addressed decisively to protect the community.
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders are responsible for upholding and enforcing these standards. They are empowered to take **immediate and appropriate action** to address any behaviors they deem unacceptable under this Code of Conduct. These actions are taken with the goal of protecting the community and preserving its safe, positive, and productive environment.
## Scope
This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
This Code of Conduct applies to all community spaces, including forums, repositories, social media accounts, and in-person events. It also applies when an individual represents the community in public settings, such as conferences or official communications.
## Enforcement
Additionally, any behavior outside of these defined spaces that negatively impacts the community or its members may fall within the scope of this Code of Conduct.
Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@openwebui.com. All complaints will be reviewed and investigated promptly and fairly.
## Reporting Violations
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
Instances of unacceptable behavior can be reported to the leadership team at **hello@openwebui.com**. Reports will be handled promptly, confidentially, and with consideration for the safety and well-being of the reporter.
All community leaders are required to uphold confidentiality and impartiality when addressing reports of violations.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### Ban
### 1. Temporary Ban
**Community Impact**: Community leaders will issue a ban to any participant whose behavior is deemed unacceptable according to this Code of Conduct. Bans are enforced immediately and without prior notice.
**Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
A ban may be temporary or permanent, depending on the severity of the violation. This includes—but is not limited to—behavior such as:
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
- Harassment or abusive behavior toward contributors.
- Persistent negativity or hostility that disrupts the collaborative environment.
- Disrespectful, demanding, or aggressive interactions with others.
- Attempts to cause harm or sabotage the community.
### 2. Permanent Ban
**Consequence**: A banned individual is immediately removed from access to all community spaces, communication channels, and events. Community leaders reserve the right to enforce either a time-limited suspension or a permanent ban based on the specific circumstances of the violation.
**Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
This approach ensures that disruptive behaviors are addressed swiftly and decisively in order to maintain the integrity and productivity of the community.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## 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.
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.
Our expectations are clear, and our enforcement reflects our commitment to this project's long-term success.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+15 -5
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Initialize device type args
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
# use build args in the docker build command with --build-arg="BUILDARG=true"
ARG USE_CUDA=false
ARG USE_OLLAMA=false
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
@@ -11,13 +11,17 @@ ARG USE_CUDA_VER=cu121
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL=""
# Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
ARG BUILD_HASH=dev-build
# Override at your own risk - non-root configurations are untested
ARG UID=0
ARG GID=0
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
ARG BUILD_HASH
WORKDIR /app
@@ -30,7 +34,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
RUN npm run build
######## WebUI backend ########
FROM python:3.11-slim-bookworm as base
FROM python:3.11-slim-bookworm AS base
# Use args
ARG USE_CUDA
@@ -72,6 +76,10 @@ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
## Tiktoken model settings ##
ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
## Hugging Face download cache ##
ENV HF_HOME="/app/backend/data/cache/embedding/models"
@@ -82,7 +90,7 @@ ENV HF_HOME="/app/backend/data/cache/embedding/models"
WORKDIR /app/backend
ENV HOME /root
ENV HOME=/root
# Create user and group if not root
RUN if [ $UID -ne 0 ]; then \
if [ $GID -ne 0 ]; then \
@@ -131,11 +139,13 @@ RUN pip3 install uv && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
else \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
fi; \
chown -R $UID:$GID /app/backend/data/
@@ -161,6 +171,6 @@ USER $UID:$GID
ARG BUILD_HASH
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
ENV DOCKER true
ENV DOCKER=true
CMD [ "bash", "start.sh"]
+30 -20
View File
@@ -1,4 +1,4 @@
# Open WebUI (Formerly Ollama WebUI) 👋
# Open WebUI 👋
![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
@@ -21,7 +21,7 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
- 🛡️ **Granular Permissions and User Groups**: By allowing administrators to create detailed user roles and permissions, we ensure a secure user environment. This granularity not only enhances security but also allows for customized user experiences, fostering a sense of ownership and responsibility amongst users.
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
@@ -37,7 +37,7 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch` and `SearchApi` and inject the results directly into your chat experience.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch`, `SearchApi` and `Bing` and inject the results directly into your chat experience.
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
@@ -49,6 +49,8 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
@@ -59,11 +61,31 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
## How to Install 🚀
> [!NOTE]
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
### Installation via Python pip 🐍
Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
1. **Install Open WebUI**:
Open your terminal and run the following command to install Open WebUI:
```bash
pip install open-webui
```
2. **Running Open WebUI**:
After installation, you can start Open WebUI by executing:
```bash
open-webui serve
```
This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
### Quick Start with Docker 🐳
> [!NOTE]
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
> [!WARNING]
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
@@ -86,7 +108,7 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
- **To run Open WebUI with Nvidia GPU support**, use this command:
- **To run Open WebUI with Nvidia GPU support**, use this command:
```bash
docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
@@ -150,7 +172,7 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
In the last part of the command, replace `open-webui` with your container name if it is different.
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/).
### Using the Dev Branch 🌙
@@ -167,18 +189,6 @@ docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --a
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
## Supporters ✨
A big shoutout to our amazing supporters who's helping to make this project possible! 🙏
### Platinum Sponsors 🤍
- We're looking for Sponsors!
### Acknowledgments
Special thanks to [Prof. Lawrence Kim](https://www.lhkim.com/) and [Prof. Nick Vincent](https://www.nickmvincent.com/) for their invaluable support and guidance in shaping this project into a research endeavor. Grateful for your mentorship throughout the journey! 🙌
## License 📜
This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
@@ -200,4 +210,4 @@ If you have any questions, suggestions, or need assistance, please open an issue
---
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
Created by [Timothy Jaeryang Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
+1 -1
View File
@@ -18,7 +18,7 @@ If you're experiencing connection issues, its often due to the WebUI docker c
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
### Error on Slow Reponses for Ollama
### Error on Slow Responses for Ollama
Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
+1
View File
@@ -52,6 +52,7 @@ def serve(
)
os.environ["USE_CUDA_DOCKER"] = "false"
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
import open_webui.main # we need set environment variables before importing main
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
+270 -150
View File
@@ -5,7 +5,11 @@ import os
import uuid
from functools import lru_cache
from pathlib import Path
from pydub import AudioSegment
from pydub.silence import split_on_silence
import aiohttp
import aiofiles
import requests
from open_webui.config import (
AUDIO_STT_ENGINE,
@@ -30,17 +34,33 @@ from open_webui.config import (
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE
from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
DEVICE_TYPE,
ENABLE_FORWARD_USER_INFO_HEADERS,
)
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from open_webui.utils.utils import get_admin_user, get_current_user, get_verified_user
from open_webui.utils.utils import get_admin_user, get_verified_user
# Constants
MAX_FILE_SIZE_MB = 25
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
app = FastAPI()
app = FastAPI(
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
)
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ALLOW_ORIGIN,
@@ -56,6 +76,9 @@ app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
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.faster_whisper_model = None
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
@@ -64,6 +87,10 @@ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
app.state.speech_synthesiser = None
app.state.speech_speaker_embeddings_dataset = None
app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
@@ -75,6 +102,31 @@ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
def set_faster_whisper_model(model: str, auto_update: bool = False):
if model and app.state.config.STT_ENGINE == "":
from faster_whisper import WhisperModel
faster_whisper_kwargs = {
"model_size_or_path": model,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not auto_update,
}
try:
app.state.faster_whisper_model = WhisperModel(**faster_whisper_kwargs)
except Exception:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
faster_whisper_kwargs["local_files_only"] = False
app.state.faster_whisper_model = WhisperModel(**faster_whisper_kwargs)
else:
app.state.faster_whisper_model = None
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
@@ -92,6 +144,7 @@ class STTConfigForm(BaseModel):
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
WHISPER_MODEL: str
class AudioConfigUpdateForm(BaseModel):
@@ -145,6 +198,7 @@ async def get_audio_config(user=Depends(get_admin_user)):
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
"ENGINE": app.state.config.STT_ENGINE,
"MODEL": app.state.config.STT_MODEL,
"WHISPER_MODEL": app.state.config.WHISPER_MODEL,
},
}
@@ -169,6 +223,8 @@ async def update_audio_config(
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
app.state.config.STT_ENGINE = form_data.stt.ENGINE
app.state.config.STT_MODEL = form_data.stt.MODEL
app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
set_faster_whisper_model(form_data.stt.WHISPER_MODEL, WHISPER_MODEL_AUTO_UPDATE)
return {
"tts": {
@@ -187,10 +243,26 @@ async def update_audio_config(
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
"ENGINE": app.state.config.STT_ENGINE,
"MODEL": app.state.config.STT_MODEL,
"WHISPER_MODEL": app.state.config.WHISPER_MODEL,
},
}
def load_speech_pipeline():
from transformers import pipeline
from datasets import load_dataset
if app.state.speech_synthesiser is None:
app.state.speech_synthesiser = pipeline(
"text-to-speech", "microsoft/speecht5_tts"
)
if app.state.speech_speaker_embeddings_dataset is None:
app.state.speech_speaker_embeddings_dataset = load_dataset(
"Matthijs/cmu-arctic-xvectors", split="validation"
)
@app.post("/speech")
async def speech(request: Request, user=Depends(get_verified_user)):
body = await request.body()
@@ -208,6 +280,12 @@ async def speech(request: Request, user=Depends(get_verified_user)):
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
try:
body = body.decode("utf-8")
body = json.loads(body)
@@ -216,46 +294,39 @@ async def speech(request: Request, user=Depends(get_verified_user)):
except Exception:
pass
r = None
try:
r = requests.post(
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
)
async with aiohttp.ClientSession() as session:
async with session.post(
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
) as r:
r.raise_for_status()
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
r.raise_for_status()
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
# Save the streaming content to a file
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
# Return the saved file
return FileResponse(file_path)
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
try:
if r.status != 200:
res = await r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
status_code=getattr(r, "status", 500),
detail=error_detail,
)
elif app.state.config.TTS_ENGINE == "elevenlabs":
payload = None
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
@@ -263,7 +334,6 @@ async def speech(request: Request, user=Depends(get_verified_user)):
raise HTTPException(status_code=400, detail="Invalid JSON payload")
voice_id = payload.get("voice", "")
if voice_id not in get_available_voices():
raise HTTPException(
status_code=400,
@@ -271,13 +341,11 @@ async def speech(request: Request, user=Depends(get_verified_user)):
)
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
headers = {
"Accept": "audio/mpeg",
"Content-Type": "application/json",
"xi-api-key": app.state.config.TTS_API_KEY,
}
data = {
"text": payload["input"],
"model_id": app.state.config.TTS_MODEL,
@@ -285,39 +353,34 @@ async def speech(request: Request, user=Depends(get_verified_user)):
}
try:
r = requests.post(url, json=data, headers=headers)
async with aiohttp.ClientSession() as session:
async with session.post(url, json=data, headers=headers) as r:
r.raise_for_status()
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
r.raise_for_status()
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
# Save the streaming content to a file
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
# Return the saved file
return FileResponse(file_path)
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
try:
if r.status != 200:
res = await r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
status_code=getattr(r, "status", 500),
detail=error_detail,
)
elif app.state.config.TTS_ENGINE == "azure":
payload = None
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
@@ -340,23 +403,138 @@ async def speech(request: Request, user=Depends(get_verified_user)):
<voice name="{language}">{payload["input"]}</voice>
</speak>"""
response = requests.post(url, headers=headers, data=data)
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, data=data) as response:
if response.status == 200:
async with aiofiles.open(file_path, "wb") as f:
await f.write(await response.read())
return FileResponse(file_path)
else:
error_msg = f"Error synthesizing speech - {response.reason}"
log.error(error_msg)
raise HTTPException(status_code=500, detail=error_msg)
except Exception as e:
log.exception(e)
raise HTTPException(status_code=500, detail=str(e))
elif app.state.config.TTS_ENGINE == "transformers":
payload = None
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
if response.status_code == 200:
with open(file_path, "wb") as f:
f.write(response.content)
return FileResponse(file_path)
else:
log.error(f"Error synthesizing speech - {response.reason}")
raise HTTPException(
status_code=500, detail=f"Error synthesizing speech - {response.reason}"
import torch
import soundfile as sf
load_speech_pipeline()
embeddings_dataset = app.state.speech_speaker_embeddings_dataset
speaker_index = 6799
try:
speaker_index = embeddings_dataset["filename"].index(
app.state.config.TTS_MODEL
)
except Exception:
pass
speaker_embedding = torch.tensor(
embeddings_dataset[speaker_index]["xvector"]
).unsqueeze(0)
speech = app.state.speech_synthesiser(
payload["input"],
forward_params={"speaker_embeddings": speaker_embedding},
)
sf.write(file_path, speech["audio"], samplerate=speech["sampling_rate"])
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
return FileResponse(file_path)
def transcribe(file_path):
print("transcribe", file_path)
filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
id = filename.split(".")[0]
if app.state.config.STT_ENGINE == "":
if app.state.faster_whisper_model is None:
set_faster_whisper_model(app.state.config.WHISPER_MODEL)
model = app.state.faster_whisper_model
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
)
transcript = "".join([segment.text for segment in list(segments)])
data = {"text": transcript.strip()}
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
log.debug(data)
return data
elif app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
print("is_mp4_audio")
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)
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
files = {"file": (filename, open(file_path, "rb"))}
data = {"model": app.state.config.STT_MODEL}
log.debug(files, data)
r = None
try:
r = requests.post(
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
headers=headers,
files=files,
data=data,
)
r.raise_for_status()
data = r.json()
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
raise Exception(error_detail)
@app.post("/transcriptions")
def transcribe(
def transcription(
file: UploadFile = File(...),
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
log.info(f"file.content_type: {file.content_type}")
@@ -368,112 +546,54 @@ def transcribe(
try:
ext = file.filename.split(".")[-1]
id = uuid.uuid4()
filename = f"{id}.{ext}"
contents = file.file.read()
file_dir = f"{CACHE_DIR}/audio/transcriptions"
os.makedirs(file_dir, exist_ok=True)
file_path = f"{file_dir}/{filename}"
print(filename)
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
f.close()
if app.state.config.STT_ENGINE == "":
from faster_whisper import WhisperModel
try:
if os.path.getsize(file_path) > MAX_FILE_SIZE: # file is bigger than 25MB
log.debug(f"File size is larger than {MAX_FILE_SIZE_MB}MB")
audio = AudioSegment.from_file(file_path)
audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio
compressed_path = f"{file_dir}/{id}_compressed.opus"
audio.export(compressed_path, format="opus", bitrate="32k")
log.debug(f"Compressed audio to {compressed_path}")
file_path = compressed_path
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
if (
os.path.getsize(file_path) > MAX_FILE_SIZE
): # Still larger than 25MB after compression
log.debug(
f"Compressed file size is still larger than {MAX_FILE_SIZE_MB}MB: {os.path.getsize(file_path)}"
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_TOO_LARGE(
size=f"{MAX_FILE_SIZE_MB}MB"
),
)
log.debug(f"whisper_kwargs: {whisper_kwargs}")
data = transcribe(file_path)
else:
data = transcribe(file_path)
try:
model = WhisperModel(**whisper_kwargs)
except Exception:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
file_path = file_path.split("/")[-1]
return {**data, "filename": file_path}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
transcript = "".join([segment.text for segment in list(segments)])
data = {"text": transcript.strip()}
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
elif app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
print("is_mp4_audio")
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)
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
files = {"file": (filename, open(file_path, "rb"))}
data = {"model": app.state.config.STT_MODEL}
print(files, data)
r = None
try:
r = requests.post(
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
headers=headers,
files=files,
data=data,
)
r.raise_for_status()
data = r.json()
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
except Exception as e:
log.exception(e)
+14 -2
View File
@@ -35,7 +35,8 @@ from open_webui.config import (
AppConfig,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import ENV, SRC_LOG_LEVELS, ENABLE_FORWARD_USER_INFO_HEADERS
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
@@ -47,7 +48,12 @@ log.setLevel(SRC_LOG_LEVELS["IMAGES"])
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI()
app = FastAPI(
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
)
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ALLOW_ORIGIN,
@@ -456,6 +462,12 @@ async def image_generations(
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
data = {
"model": (
app.state.config.MODEL
@@ -125,22 +125,34 @@ async def comfyui_generate_image(
workflow[node_id]["inputs"][node.key] = model
elif node.type == "prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["text"] = payload.prompt
workflow[node_id]["inputs"][
node.key if node.key else "text"
] = payload.prompt
elif node.type == "negative_prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["text"] = payload.negative_prompt
workflow[node_id]["inputs"][
node.key if node.key else "text"
] = payload.negative_prompt
elif node.type == "width":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["width"] = payload.width
workflow[node_id]["inputs"][
node.key if node.key else "width"
] = payload.width
elif node.type == "height":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["height"] = payload.height
workflow[node_id]["inputs"][
node.key if node.key else "height"
] = payload.height
elif node.type == "n":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["batch_size"] = payload.n
workflow[node_id]["inputs"][
node.key if node.key else "batch_size"
] = payload.n
elif node.type == "steps":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["steps"] = payload.steps
workflow[node_id]["inputs"][
node.key if node.key else "steps"
] = payload.steps
elif node.type == "seed":
seed = (
payload.seed
File diff suppressed because it is too large Load Diff
+303 -131
View File
@@ -6,39 +6,54 @@ from pathlib import Path
from typing import Literal, Optional, overload
import aiohttp
from aiocache import cached
import requests
from open_webui.apps.webui.models.models import Models
from open_webui.config import (
AIOHTTP_CLIENT_TIMEOUT,
CACHE_DIR,
CORS_ALLOW_ORIGIN,
ENABLE_MODEL_FILTER,
ENABLE_OPENAI_API,
MODEL_FILTER_LIST,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
OPENAI_API_CONFIGS,
AppConfig,
)
from open_webui.env import (
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
ENABLE_FORWARD_USER_INFO_HEADERS,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import ENV, SRC_LOG_LEVELS
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
from starlette.background import BackgroundTask
from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
app = FastAPI()
app = FastAPI(
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
)
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ALLOW_ORIGIN,
@@ -47,72 +62,68 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
app.state.MODELS = {}
@app.middleware("http")
async def check_url(request: Request, call_next):
if len(app.state.MODELS) == 0:
await get_all_models()
response = await call_next(request)
return response
app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
@app.get("/config")
async def get_config(user=Depends(get_admin_user)):
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
return {
"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API,
"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS,
"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS,
"OPENAI_API_CONFIGS": app.state.config.OPENAI_API_CONFIGS,
}
class OpenAIConfigForm(BaseModel):
enable_openai_api: Optional[bool] = None
ENABLE_OPENAI_API: Optional[bool] = None
OPENAI_API_BASE_URLS: list[str]
OPENAI_API_KEYS: list[str]
OPENAI_API_CONFIGS: dict
@app.post("/config/update")
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
app.state.config.ENABLE_OPENAI_API = form_data.ENABLE_OPENAI_API
app.state.config.OPENAI_API_BASE_URLS = form_data.OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = form_data.OPENAI_API_KEYS
class UrlsUpdateForm(BaseModel):
urls: list[str]
# Check if API KEYS length is same than API URLS length
if len(app.state.config.OPENAI_API_KEYS) != len(
app.state.config.OPENAI_API_BASE_URLS
):
if len(app.state.config.OPENAI_API_KEYS) > len(
app.state.config.OPENAI_API_BASE_URLS
):
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
: len(app.state.config.OPENAI_API_BASE_URLS)
]
else:
app.state.config.OPENAI_API_KEYS += [""] * (
len(app.state.config.OPENAI_API_BASE_URLS)
- len(app.state.config.OPENAI_API_KEYS)
)
app.state.config.OPENAI_API_CONFIGS = form_data.OPENAI_API_CONFIGS
class KeysUpdateForm(BaseModel):
keys: list[str]
# Remove any extra configs
config_urls = app.state.config.OPENAI_API_CONFIGS.keys()
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS):
if url not in config_urls:
app.state.config.OPENAI_API_CONFIGS.pop(url, None)
@app.get("/urls")
async def get_openai_urls(user=Depends(get_admin_user)):
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
@app.post("/urls/update")
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
await get_all_models()
app.state.config.OPENAI_API_BASE_URLS = form_data.urls
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
@app.get("/keys")
async def get_openai_keys(user=Depends(get_admin_user)):
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
@app.post("/keys/update")
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
app.state.config.OPENAI_API_KEYS = form_data.keys
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
return {
"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API,
"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS,
"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS,
"OPENAI_API_CONFIGS": app.state.config.OPENAI_API_CONFIGS,
}
@app.post("/audio/speech")
@@ -138,6 +149,11 @@ async def speech(request: Request, user=Depends(get_verified_user)):
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
headers["HTTP-Referer"] = "https://openwebui.com/"
headers["X-Title"] = "Open WebUI"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
r = None
try:
r = requests.post(
@@ -179,10 +195,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
async def fetch_url(url, key):
timeout = aiohttp.ClientTimeout(total=5)
async def aiohttp_get(url, key=None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
try:
headers = {"Authorization": f"Bearer {key}"}
headers = {"Authorization": f"Bearer {key}"} if key else {}
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(url, headers=headers) as response:
return await response.json()
@@ -237,14 +253,8 @@ def merge_models_lists(model_lists):
return merged_list
def is_openai_api_disabled():
api_keys = app.state.config.OPENAI_API_KEYS
no_keys = len(api_keys) == 1 and api_keys[0] == ""
return no_keys or not app.state.config.ENABLE_OPENAI_API
async def get_all_models_raw() -> list:
if is_openai_api_disabled():
async def get_all_models_responses() -> list:
if not app.state.config.ENABLE_OPENAI_API:
return []
# Check if API KEYS length is same than API URLS length
@@ -260,33 +270,72 @@ async def get_all_models_raw() -> list:
else:
app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys)
tasks = [
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
]
tasks = []
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS):
if url not in app.state.config.OPENAI_API_CONFIGS:
tasks.append(
aiohttp_get(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
)
else:
api_config = app.state.config.OPENAI_API_CONFIGS.get(url, {})
enable = api_config.get("enable", True)
model_ids = api_config.get("model_ids", [])
if enable:
if len(model_ids) == 0:
tasks.append(
aiohttp_get(
f"{url}/models", app.state.config.OPENAI_API_KEYS[idx]
)
)
else:
model_list = {
"object": "list",
"data": [
{
"id": model_id,
"name": model_id,
"owned_by": "openai",
"openai": {"id": model_id},
"urlIdx": idx,
}
for model_id in model_ids
],
}
tasks.append(asyncio.ensure_future(asyncio.sleep(0, model_list)))
else:
tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
responses = await asyncio.gather(*tasks)
for idx, response in enumerate(responses):
if response:
url = app.state.config.OPENAI_API_BASE_URLS[idx]
api_config = app.state.config.OPENAI_API_CONFIGS.get(url, {})
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
model["id"] = f"{prefix_id}.{model['id']}"
log.debug(f"get_all_models:responses() {responses}")
return responses
@overload
async def get_all_models(raw: Literal[True]) -> list: ...
@overload
async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
async def get_all_models(raw=False) -> dict[str, list] | list:
@cached(ttl=3)
async def get_all_models() -> dict[str, list]:
log.info("get_all_models()")
if is_openai_api_disabled():
return [] if raw else {"data": []}
responses = await get_all_models_raw()
if raw:
return responses
if not app.state.config.ENABLE_OPENAI_API:
return {"data": []}
responses = await get_all_models_responses()
def extract_data(response):
if response and "data" in response:
@@ -296,9 +345,7 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
return None
models = {"data": merge_models_lists(map(extract_data, responses))}
log.debug(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]}
return models
@@ -306,18 +353,12 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
@app.get("/models")
@app.get("/models/{url_idx}")
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
models = {
"data": [],
}
if url_idx is None:
models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
models["data"] = list(
filter(
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
models["data"],
)
)
return models
return models
else:
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = app.state.config.OPENAI_API_KEYS[url_idx]
@@ -326,56 +367,126 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
r = None
try:
r = requests.request(method="GET", url=f"{url}/models", headers=headers)
r.raise_for_status()
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.get(f"{url}/models", headers=headers) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = r.json()
response_data = await r.json()
if "api.openai.com" in url:
# Filter the response data
response_data["data"] = [
model
for model in response_data["data"]
if not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
# Check if we're calling OpenAI API based on the URL
if "api.openai.com" in url:
# Filter models according to the specified conditions
response_data["data"] = [
model
for model in response_data.get("data", [])
if not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
]
)
]
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
models = response_data
except aiohttp.ClientError as e:
# ClientError covers all aiohttp requests issues
log.exception(f"Client error: {str(e)}")
# Handle aiohttp-specific connection issues, timeout etc.
raise HTTPException(
status_code=500, detail="Open WebUI: Server Connection Error"
)
except Exception as e:
log.exception(f"Unexpected error: {e}")
# Generic error handler in case parsing JSON or other steps fail
error_detail = f"Unexpected error: {str(e)}"
raise HTTPException(status_code=500, detail=error_detail)
if user.role == "user":
# Filter models based on user access control
filtered_models = []
for model in models.get("data", []):
model_info = Models.get_model_by_id(model["id"])
if model_info:
if user.id == model_info.user_id or has_access(
user.id, type="read", access_control=model_info.access_control
):
filtered_models.append(model)
models["data"] = filtered_models
return models
class ConnectionVerificationForm(BaseModel):
url: str
key: str
@app.post("/verify")
async def verify_connection(
form_data: ConnectionVerificationForm, user=Depends(get_admin_user)
):
url = form_data.url
key = form_data.key
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.get(f"{url}/models", headers=headers) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
except Exception:
error_detail = f"External: {e}"
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json()
return response_data
except aiohttp.ClientError as e:
# ClientError covers all aiohttp requests issues
log.exception(f"Client error: {str(e)}")
# Handle aiohttp-specific connection issues, timeout etc.
raise HTTPException(
status_code=r.status_code if r else 500,
detail=error_detail,
status_code=500, detail="Open WebUI: Server Connection Error"
)
except Exception as e:
log.exception(f"Unexpected error: {e}")
# Generic error handler in case parsing JSON or other steps fail
error_detail = f"Unexpected error: {str(e)}"
raise HTTPException(status_code=500, detail=error_detail)
@app.post("/chat/completions")
@app.post("/chat/completions/{url_idx}")
async def generate_chat_completion(
form_data: dict,
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
bypass_filter: Optional[bool] = False,
):
idx = 0
payload = {**form_data}
@@ -386,6 +497,7 @@ async def generate_chat_completion(
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
# Check model info and override the payload
if model_info:
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
@@ -394,9 +506,52 @@ async def generate_chat_completion(
payload = apply_model_params_to_body_openai(params, payload)
payload = apply_model_system_prompt_to_body(params, payload, user)
model = app.state.MODELS[payload.get("model")]
idx = model["urlIdx"]
# Check if user has access to the model
if not bypass_filter and user.role == "user":
if not (
user.id == model_info.user_id
or has_access(
user.id, type="read", access_control=model_info.access_control
)
):
raise HTTPException(
status_code=403,
detail="Model not found",
)
elif not bypass_filter:
if user.role != "admin":
raise HTTPException(
status_code=403,
detail="Model not found",
)
# Attemp to get urlIdx from the model
models = await get_all_models()
# Find the model from the list
model = next(
(model for model in models["data"] if model["id"] == payload.get("model")),
None,
)
if model:
idx = model["urlIdx"]
else:
raise HTTPException(
status_code=404,
detail="Model not found",
)
# Get the API config for the model
api_config = app.state.config.OPENAI_API_CONFIGS.get(
app.state.config.OPENAI_API_BASE_URLS[idx], {}
)
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
# Add user info to the payload if the model is a pipeline
if "pipeline" in model and model.get("pipeline"):
payload["user"] = {
"name": user.name,
@@ -408,27 +563,39 @@ async def generate_chat_completion(
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
# Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
is_o1 = payload["model"].lower().startswith("o1-")
# Change max_completion_tokens to max_tokens (Backward compatible)
if "api.openai.com" not in url and not payload["model"].lower().startswith("o1-"):
if "api.openai.com" not in url and not is_o1:
if "max_completion_tokens" in payload:
# Remove "max_completion_tokens" from the payload
payload["max_tokens"] = payload["max_completion_tokens"]
del payload["max_completion_tokens"]
else:
if is_o1 and "max_tokens" in payload:
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
if "max_tokens" in payload and "max_completion_tokens" in payload:
del payload["max_tokens"]
# Fix: O1 does not support the "system" parameter, Modify "system" to "user"
if is_o1 and payload["messages"][0]["role"] == "system":
payload["messages"][0]["role"] = "user"
# Convert the modified body back to JSON
payload = json.dumps(payload)
log.debug(payload)
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
headers["HTTP-Referer"] = "https://openwebui.com/"
headers["X-Title"] = "Open WebUI"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
r = None
session = None
@@ -497,6 +664,11 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
r = None
session = None
@@ -1,10 +0,0 @@
from open_webui.apps.rag.vector.dbs.chroma import ChromaClient
from open_webui.apps.rag.vector.dbs.milvus import MilvusClient
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
VECTOR_DB_CLIENT = MilvusClient()
else:
VECTOR_DB_CLIENT = ChromaClient()
@@ -0,0 +1,190 @@
import requests
import logging
import ftfy
from langchain_community.document_loaders import (
BSHTMLLoader,
CSVLoader,
Docx2txtLoader,
OutlookMessageLoader,
PyPDFLoader,
TextLoader,
UnstructuredEPubLoader,
UnstructuredExcelLoader,
UnstructuredMarkdownLoader,
UnstructuredPowerPointLoader,
UnstructuredRSTLoader,
UnstructuredXMLLoader,
YoutubeLoader,
)
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
known_source_ext = [
"go",
"py",
"java",
"sh",
"bat",
"ps1",
"cmd",
"js",
"ts",
"css",
"cpp",
"hpp",
"h",
"c",
"cs",
"sql",
"log",
"ini",
"pl",
"pm",
"r",
"dart",
"dockerfile",
"env",
"php",
"hs",
"hsc",
"lua",
"nginxconf",
"conf",
"m",
"mm",
"plsql",
"perl",
"rb",
"rs",
"db2",
"scala",
"bash",
"swift",
"vue",
"svelte",
"msg",
"ex",
"exs",
"erl",
"tsx",
"jsx",
"hs",
"lhs",
]
class TikaLoader:
def __init__(self, url, file_path, mime_type=None):
self.url = url
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
if self.mime_type is not None:
headers = {"Content-Type": self.mime_type}
else:
headers = {}
endpoint = self.url
if not endpoint.endswith("/"):
endpoint += "/"
endpoint += "tika/text"
r = requests.put(endpoint, data=data, headers=headers)
if r.ok:
raw_metadata = r.json()
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
if "Content-Type" in raw_metadata:
headers["Content-Type"] = raw_metadata["Content-Type"]
log.info("Tika extracted text: %s", text)
return [Document(page_content=text, metadata=headers)]
else:
raise Exception(f"Error calling Tika: {r.reason}")
class Loader:
def __init__(self, engine: str = "", **kwargs):
self.engine = engine
self.kwargs = kwargs
def load(
self, filename: str, file_content_type: str, file_path: str
) -> list[Document]:
loader = self._get_loader(filename, file_content_type, file_path)
docs = loader.load()
return [
Document(
page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata
)
for doc in docs
]
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
):
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,
)
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)
elif file_ext == "rst":
loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml":
loader = UnstructuredXMLLoader(file_path)
elif file_ext in ["htm", "html"]:
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
elif file_ext == "md":
loader = TextLoader(file_path, autodetect_encoding=True)
elif file_content_type == "application/epub+zip":
loader = UnstructuredEPubLoader(file_path)
elif (
file_content_type
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
or file_ext == "docx"
):
loader = Docx2txtLoader(file_path)
elif file_content_type in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_content_type in [
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
] or file_ext in ["ppt", "pptx"]:
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
):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
return loader
@@ -0,0 +1,98 @@
from typing import Any, Dict, Generator, List, Optional, Sequence, Union
from urllib.parse import parse_qs, urlparse
from langchain_core.documents import Document
ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_NETLOCS = {
"youtu.be",
"m.youtube.com",
"youtube.com",
"www.youtube.com",
"www.youtube-nocookie.com",
"vid.plus",
}
def _parse_video_id(url: str) -> Optional[str]:
"""Parse a YouTube URL and return the video ID if valid, otherwise None."""
parsed_url = urlparse(url)
if parsed_url.scheme not in ALLOWED_SCHEMES:
return None
if parsed_url.netloc not in ALLOWED_NETLOCS:
return None
path = parsed_url.path
if path.endswith("/watch"):
query = parsed_url.query
parsed_query = parse_qs(query)
if "v" in parsed_query:
ids = parsed_query["v"]
video_id = ids if isinstance(ids, str) else ids[0]
else:
return None
else:
path = parsed_url.path.lstrip("/")
video_id = path.split("/")[-1]
if len(video_id) != 11: # Video IDs are 11 characters long
return None
return video_id
class YoutubeLoader:
"""Load `YouTube` video transcripts."""
def __init__(
self,
video_id: str,
language: Union[str, Sequence[str]] = "en",
):
"""Initialize with YouTube video ID."""
_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
if isinstance(language, str):
self.language = [language]
else:
self.language = language
def load(self) -> List[Document]:
"""Load YouTube transcripts into `Document` objects."""
try:
from youtube_transcript_api import (
NoTranscriptFound,
TranscriptsDisabled,
YouTubeTranscriptApi,
)
except ImportError:
raise ImportError(
'Could not import "youtube_transcript_api" Python package. '
"Please install it with `pip install youtube-transcript-api`."
)
try:
transcript_list = YouTubeTranscriptApi.list_transcripts(self.video_id)
except Exception as e:
print(e)
return []
try:
transcript = transcript_list.find_transcript(self.language)
except NoTranscriptFound:
transcript = transcript_list.find_transcript(["en"])
transcript_pieces: List[Dict[str, Any]] = transcript.fetch()
transcript = " ".join(
map(
lambda transcript_piece: transcript_piece["text"].strip(" "),
transcript_pieces,
)
)
return [Document(page_content=transcript, metadata=self._metadata)]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
import os
import torch
import numpy as np
from colbert.infra import ColBERTConfig
from colbert.modeling.checkpoint import Checkpoint
class ColBERT:
def __init__(self, name, **kwargs) -> None:
print("ColBERT: Loading model", name)
self.device = "cuda" if torch.cuda.is_available() else "cpu"
DOCKER = kwargs.get("env") == "docker"
if DOCKER:
# This is a workaround for the issue with the docker container
# where the torch extension is not loaded properly
# and the following error is thrown:
# /root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory
lock_file = (
"/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock"
)
if os.path.exists(lock_file):
os.remove(lock_file)
self.ckpt = Checkpoint(
name,
colbert_config=ColBERTConfig(model_name=name),
).to(self.device)
pass
def calculate_similarity_scores(self, query_embeddings, document_embeddings):
query_embeddings = query_embeddings.to(self.device)
document_embeddings = document_embeddings.to(self.device)
# Validate dimensions to ensure compatibility
if query_embeddings.dim() != 3:
raise ValueError(
f"Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}."
)
if document_embeddings.dim() != 3:
raise ValueError(
f"Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}."
)
if query_embeddings.size(0) not in [1, document_embeddings.size(0)]:
raise ValueError(
"There should be either one query or queries equal to the number of documents."
)
# Transpose the query embeddings to align for matrix multiplication
transposed_query_embeddings = query_embeddings.permute(0, 2, 1)
# Compute similarity scores using batch matrix multiplication
computed_scores = torch.matmul(document_embeddings, transposed_query_embeddings)
# Apply max pooling to extract the highest semantic similarity across each document's sequence
maximum_scores = torch.max(computed_scores, dim=1).values
# Sum up the maximum scores across features to get the overall document relevance scores
final_scores = maximum_scores.sum(dim=1)
normalized_scores = torch.softmax(final_scores, dim=0)
return normalized_scores.detach().cpu().numpy().astype(np.float32)
def predict(self, sentences):
query = sentences[0][0]
docs = [i[1] for i in sentences]
# Embedding the documents
embedded_docs = self.ckpt.docFromText(docs, bsize=32)[0]
# Embedding the queries
embedded_queries = self.ckpt.queryFromText([query], bsize=32)
embedded_query = embedded_queries[0]
# Calculate retrieval scores for the query against all documents
scores = self.calculate_similarity_scores(
embedded_query.unsqueeze(0), embedded_docs
)
return scores
@@ -3,6 +3,7 @@ import os
import uuid
from typing import Optional, Union
import asyncio
import requests
from huggingface_hub import snapshot_download
@@ -10,17 +11,11 @@ from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriev
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
from open_webui.apps.ollama.main import (
GenerateEmbeddingsForm,
generate_ollama_embeddings,
)
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.misc import get_last_user_message
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -65,20 +60,17 @@ class VectorSearchRetriever(BaseRetriever):
def query_doc(
collection_name: str,
query: str,
embedding_function,
query_embedding: list[float],
k: int,
):
try:
result = VECTOR_DB_CLIENT.search(
collection_name=collection_name,
vectors=[embedding_function(query)],
vectors=[query_embedding],
limit=k,
)
print("result", result)
log.info(f"query_doc:result {result}")
log.info(f"query_doc:result {result.ids} {result.metadatas}")
return result
except Exception as e:
print(e)
@@ -129,7 +121,10 @@ def query_doc_with_hybrid_search(
"metadatas": [[d.metadata for d in result]],
}
log.info(f"query_doc_with_hybrid_search:result {result}")
log.info(
"query_doc_with_hybrid_search:result "
+ f'{result["metadatas"]} {result["distances"]}'
)
return result
except Exception as e:
raise e
@@ -180,32 +175,34 @@ def merge_and_sort_query_results(
def query_collection(
collection_names: list[str],
query: str,
queries: list[str],
embedding_function,
k: int,
) -> dict:
results = []
for collection_name in collection_names:
if collection_name:
try:
result = query_doc(
collection_name=collection_name,
query=query,
k=k,
embedding_function=embedding_function,
)
results.append(result.model_dump())
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
else:
pass
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
return merge_and_sort_query_results(results, k=k)
def query_collection_with_hybrid_search(
collection_names: list[str],
query: str,
queries: list[str],
embedding_function,
k: int,
reranking_function,
@@ -215,15 +212,16 @@ def query_collection_with_hybrid_search(
error = False
for collection_name in collection_names:
try:
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)
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}"
@@ -238,160 +236,135 @@ def query_collection_with_hybrid_search(
return merge_and_sort_query_results(results, k=k, reverse=True)
def rag_template(template: str, context: str, query: str):
count = template.count("[context]")
assert "[context]" in template, "RAG template does not contain '[context]'"
if "<context>" in context and "</context>" in context:
log.debug(
"WARNING: Potential prompt injection attack: the RAG "
"context contains '<context>' and '</context>'. This might be "
"nothing, or the user might be trying to hack something."
)
if "[query]" in context:
query_placeholder = f"[query-{str(uuid.uuid4())}]"
template = template.replace("[query]", query_placeholder)
template = template.replace("[context]", context)
template = template.replace(query_placeholder, query)
else:
template = template.replace("[context]", context)
template = template.replace("[query]", query)
return template
def get_embedding_function(
embedding_engine,
embedding_model,
embedding_function,
openai_key,
openai_url,
batch_size,
url,
key,
embedding_batch_size,
):
if embedding_engine == "":
return lambda query: embedding_function.encode(query).tolist()
elif embedding_engine in ["ollama", "openai"]:
if embedding_engine == "ollama":
func = lambda query: generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": embedding_model,
"prompt": query,
}
)
)
elif embedding_engine == "openai":
func = lambda query: generate_openai_embeddings(
model=embedding_model,
text=query,
key=openai_key,
url=openai_url,
)
func = lambda query: generate_embeddings(
engine=embedding_engine,
model=embedding_model,
text=query,
url=url,
key=key,
)
def generate_multiple(query, f):
def generate_multiple(query, func):
if isinstance(query, list):
if embedding_engine == "openai":
embeddings = []
for i in range(0, len(query), batch_size):
embeddings.extend(f(query[i : i + batch_size]))
return embeddings
else:
return [f(q) for q in query]
embeddings = []
for i in range(0, len(query), embedding_batch_size):
embeddings.extend(func(query[i : i + embedding_batch_size]))
return embeddings
else:
return f(query)
return func(query)
return lambda query: generate_multiple(query, func)
def get_rag_context(
def get_sources_from_files(
files,
messages,
queries,
embedding_function,
k,
reranking_function,
r,
hybrid_search,
):
log.debug(f"files: {files} {messages} {embedding_function} {reranking_function}")
query = get_last_user_message(messages)
log.debug(f"files: {files} {queries} {embedding_function} {reranking_function}")
extracted_collections = []
relevant_contexts = []
for file in files:
context = None
collection_names = (
file["collection_names"]
if file["type"] == "collection"
else [file["collection_name"]] if file["collection_name"] else []
)
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {file} as it has already been extracted")
continue
try:
if file.get("context") == "full":
context = {
"documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
}
else:
context = None
if file["type"] == "text":
context = file["content"]
else:
if hybrid_search:
try:
context = query_collection_with_hybrid_search(
collection_names = []
if file.get("type") == "collection":
if file.get("legacy"):
collection_names = file.get("collection_names", [])
else:
collection_names.append(file["id"])
elif file.get("collection_name"):
collection_names.append(file["collection_name"])
elif file.get("id"):
if file.get("legacy"):
collection_names.append(f"{file['id']}")
else:
collection_names.append(f"file-{file['id']}")
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {file} as it has already been extracted")
continue
try:
context = None
if file.get("type") == "text":
context = file["content"]
else:
if hybrid_search:
try:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
queries=queries,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
except Exception as e:
log.debug(
"Error when using hybrid search, using"
" non hybrid search as fallback."
)
if (not hybrid_search) or (context is None):
context = query_collection(
collection_names=collection_names,
query=query,
queries=queries,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
except Exception as e:
log.debug(
"Error when using hybrid search, using"
" non hybrid search as fallback."
)
except Exception as e:
log.exception(e)
if (not hybrid_search) or (context is None):
context = query_collection(
collection_names=collection_names,
query=query,
embedding_function=embedding_function,
k=k,
)
except Exception as e:
log.exception(e)
extracted_collections.extend(collection_names)
if context:
relevant_contexts.append({**context, "source": file})
extracted_collections.extend(collection_names)
contexts = []
citations = []
if "data" in file:
del file["data"]
relevant_contexts.append({**context, "file": file})
sources = []
for context in relevant_contexts:
try:
if "documents" in context:
contexts.append(
"\n\n".join(
[text for text in context["documents"][0] if text is not None]
)
)
if "metadatas" in context:
citations.append(
{
"source": context["source"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
}
)
source = {
"source": context["file"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
}
if "distances" in context and context["distances"]:
source["distances"] = context["distances"][0]
sources.append(source)
except Exception as e:
log.exception(e)
return contexts, citations
return sources
def get_model_path(model: str, update_model: bool = False):
@@ -432,22 +405,8 @@ def get_model_path(model: str, update_model: bool = False):
return model
def generate_openai_embeddings(
model: str,
text: Union[str, list[str]],
key: str,
url: str = "https://api.openai.com/v1",
):
if isinstance(text, list):
embeddings = generate_openai_batch_embeddings(model, text, key, url)
else:
embeddings = generate_openai_batch_embeddings(model, [text], key, url)
return embeddings[0] if isinstance(text, str) else embeddings
def generate_openai_batch_embeddings(
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
model: str, texts: list[str], url: str = "https://api.openai.com/v1", key: str = ""
) -> Optional[list[list[float]]]:
try:
r = requests.post(
@@ -469,6 +428,53 @@ def generate_openai_batch_embeddings(
return None
def generate_ollama_batch_embeddings(
model: str, texts: list[str], url: str, key: str = ""
) -> Optional[list[list[float]]]:
try:
r = requests.post(
f"{url}/api/embed",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {key}",
},
json={"input": texts, "model": model},
)
r.raise_for_status()
data = r.json()
if "embeddings" in data:
return data["embeddings"]
else:
raise "Something went wrong :/"
except Exception as e:
print(e)
return None
def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **kwargs):
url = kwargs.get("url", "")
key = kwargs.get("key", "")
if engine == "ollama":
if isinstance(text, list):
embeddings = generate_ollama_batch_embeddings(
**{"model": model, "texts": text, "url": url, "key": key}
)
else:
embeddings = generate_ollama_batch_embeddings(
**{"model": model, "texts": [text], "url": url, "key": key}
)
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)
else:
embeddings = generate_openai_batch_embeddings(model, [text], url, key)
return embeddings[0] if isinstance(text, str) else embeddings
import operator
from typing import Optional, Sequence
@@ -0,0 +1,22 @@
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
from open_webui.apps.retrieval.vector.dbs.milvus import MilvusClient
VECTOR_DB_CLIENT = MilvusClient()
elif VECTOR_DB == "qdrant":
from open_webui.apps.retrieval.vector.dbs.qdrant import QdrantClient
VECTOR_DB_CLIENT = QdrantClient()
elif VECTOR_DB == "opensearch":
from open_webui.apps.retrieval.vector.dbs.opensearch import OpenSearchClient
VECTOR_DB_CLIENT = OpenSearchClient()
elif VECTOR_DB == "pgvector":
from open_webui.apps.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
else:
from open_webui.apps.retrieval.vector.dbs.chroma import ChromaClient
VECTOR_DB_CLIENT = ChromaClient()
@@ -4,7 +4,7 @@ from chromadb.utils.batch_utils import create_batches
from typing import Optional
from open_webui.apps.rag.vector.main import VectorItem, SearchResult, GetResult
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
CHROMA_DATA_PATH,
CHROMA_HTTP_HOST,
@@ -13,11 +13,24 @@ from open_webui.config import (
CHROMA_HTTP_SSL,
CHROMA_TENANT,
CHROMA_DATABASE,
CHROMA_CLIENT_AUTH_PROVIDER,
CHROMA_CLIENT_AUTH_CREDENTIALS,
)
class ChromaClient:
def __init__(self):
settings_dict = {
"allow_reset": True,
"anonymized_telemetry": False,
}
if CHROMA_CLIENT_AUTH_PROVIDER is not None:
settings_dict["chroma_client_auth_provider"] = CHROMA_CLIENT_AUTH_PROVIDER
if CHROMA_CLIENT_AUTH_CREDENTIALS is not None:
settings_dict["chroma_client_auth_credentials"] = (
CHROMA_CLIENT_AUTH_CREDENTIALS
)
if CHROMA_HTTP_HOST != "":
self.client = chromadb.HttpClient(
host=CHROMA_HTTP_HOST,
@@ -26,12 +39,12 @@ class ChromaClient:
ssl=CHROMA_HTTP_SSL,
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
settings=Settings(**settings_dict),
)
else:
self.client = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
settings=Settings(**settings_dict),
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
)
@@ -49,22 +62,49 @@ class ChromaClient:
self, collection_name: str, vectors: list[list[float | int]], limit: int
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
collection = self.client.get_collection(name=collection_name)
if collection:
result = collection.query(
query_embeddings=vectors,
n_results=limit,
)
try:
collection = self.client.get_collection(name=collection_name)
if collection:
result = collection.query(
query_embeddings=vectors,
n_results=limit,
)
return SearchResult(
**{
"ids": result["ids"],
"distances": result["distances"],
"documents": result["documents"],
"metadatas": result["metadatas"],
}
)
return None
return SearchResult(
**{
"ids": result["ids"],
"distances": result["distances"],
"documents": result["documents"],
"metadatas": result["metadatas"],
}
)
return None
except Exception as e:
return None
def query(
self, collection_name: str, filter: dict, limit: Optional[int] = None
) -> Optional[GetResult]:
# Query the items from the collection based on the filter.
try:
collection = self.client.get_collection(name=collection_name)
if collection:
result = collection.get(
where=filter,
limit=limit,
)
return GetResult(
**{
"ids": [result["ids"]],
"documents": [result["documents"]],
"metadatas": [result["metadatas"]],
}
)
return None
except Exception as e:
print(e)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
@@ -82,7 +122,9 @@ class ChromaClient:
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.
collection = self.client.get_or_create_collection(name=collection_name)
collection = self.client.get_or_create_collection(
name=collection_name, metadata={"hnsw:space": "cosine"}
)
ids = [item["id"] for item in items]
documents = [item["text"] for item in items]
@@ -100,7 +142,9 @@ class ChromaClient:
def upsert(self, collection_name: str, items: list[VectorItem]):
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
collection = self.client.get_or_create_collection(name=collection_name)
collection = self.client.get_or_create_collection(
name=collection_name, metadata={"hnsw:space": "cosine"}
)
ids = [item["id"] for item in items]
documents = [item["text"] for item in items]
@@ -111,11 +155,19 @@ class ChromaClient:
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
)
def delete(self, collection_name: str, ids: list[str]):
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
collection = self.client.get_collection(name=collection_name)
if collection:
collection.delete(ids=ids)
if ids:
collection.delete(ids=ids)
elif filter:
collection.delete(where=filter)
def reset(self):
# Resets the database. This will delete all collections and item entries.
@@ -4,7 +4,7 @@ import json
from typing import Optional
from open_webui.apps.rag.vector.main import VectorItem, SearchResult, GetResult
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
MILVUS_URI,
)
@@ -16,8 +16,6 @@ class MilvusClient:
self.client = Client(uri=MILVUS_URI)
def _result_to_get_result(self, result) -> GetResult:
print(result)
ids = []
documents = []
metadatas = []
@@ -26,7 +24,6 @@ class MilvusClient:
_ids = []
_documents = []
_metadatas = []
for item in match:
_ids.append(item.get("id"))
_documents.append(item.get("data", {}).get("text"))
@@ -45,8 +42,6 @@ class MilvusClient:
)
def _result_to_search_result(self, result) -> SearchResult:
print(result)
ids = []
distances = []
documents = []
@@ -102,7 +97,10 @@ class MilvusClient:
index_params = self.client.prepare_index_params()
index_params.add_index(
field_name="vector", index_type="HNSW", metric_type="COSINE", params={}
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 100},
)
self.client.create_collection(
@@ -113,12 +111,14 @@ class MilvusClient:
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
collection_name = collection_name.replace("-", "_")
return self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
)
def delete_collection(self, collection_name: str):
# Delete the collection based on the collection name.
collection_name = collection_name.replace("-", "_")
return self.client.drop_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
)
@@ -127,6 +127,7 @@ class MilvusClient:
self, collection_name: str, vectors: list[list[float | int]], limit: int
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
collection_name = collection_name.replace("-", "_")
result = self.client.search(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=vectors,
@@ -136,8 +137,68 @@ class MilvusClient:
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):
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
# Initialize offset and remaining to handle pagination
offset = 0
remaining = limit
try:
# Loop until there are no more items to fetch or the desired limit is reached
while remaining > 0:
print("remaining", remaining)
current_fetch = min(
max_limit, remaining
) # Determine how many items to fetch in this iteration
results = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
output_fields=["*"],
limit=current_fetch,
offset=offset,
)
if not results:
break
all_results.extend(results)
results_count = len(results)
remaining -= (
results_count # Decrease remaining by the number of items fetched
)
offset += results_count
# Break the loop if the results returned are less than the requested fetch count
if results_count < current_fetch:
break
print(all_results)
return self._result_to_get_result([all_results])
except Exception as e:
print(e)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
collection_name = collection_name.replace("-", "_")
result = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter='id != ""',
@@ -146,6 +207,7 @@ class MilvusClient:
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.
collection_name = collection_name.replace("-", "_")
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
@@ -168,6 +230,7 @@ class MilvusClient:
def upsert(self, collection_name: str, items: list[VectorItem]):
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
collection_name = collection_name.replace("-", "_")
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
@@ -188,17 +251,35 @@ class MilvusClient:
],
)
def delete(self, collection_name: str, ids: list[str]):
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
collection_name = collection_name.replace("-", "_")
if 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()
]
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
ids=ids,
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
)
def reset(self):
# Resets the database. This will delete all collections and item entries.
collection_names = self.client.list_collections()
for collection_name in collection_names:
if collection_name.startswith(self.collection_prefix):
@@ -0,0 +1,178 @@
from opensearchpy import OpenSearch
from typing import Optional
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
OPENSEARCH_URI,
OPENSEARCH_SSL,
OPENSEARCH_CERT_VERIFY,
OPENSEARCH_USERNAME,
OPENSEARCH_PASSWORD,
)
class OpenSearchClient:
def __init__(self):
self.index_prefix = "open_webui"
self.client = OpenSearch(
hosts=[OPENSEARCH_URI],
use_ssl=OPENSEARCH_SSL,
verify_certs=OPENSEARCH_CERT_VERIFY,
http_auth=(OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD),
)
def _result_to_get_result(self, result) -> GetResult:
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)
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
)
def _create_index(self, index_name: str, dimension: int):
body = {
"mappings": {
"properties": {
"id": {"type": "keyword"},
"vector": {
"type": "dense_vector",
"dims": dimension, # Adjust based on your vector dimensions
"index": true,
"similarity": "faiss",
"method": {
"name": "hnsw",
"space_type": "ip", # Use inner product to approximate cosine similarity
"engine": "faiss",
"ef_construction": 128,
"m": 16,
},
},
"text": {"type": "text"},
"metadata": {"type": "object"},
}
}
}
self.client.indices.create(index=f"{self.index_prefix}_{index_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:
# 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}")
def delete_colleciton(self, index_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}")
def search(
self, index_name: str, vectors: list[list[float]], 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
},
}
},
}
result = self.client.search(
index=f"{self.index_prefix}_{index_name}", body=query
)
return self._result_to_search_result(result)
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 get(self, index_name: str) -> Optional[GetResult]:
query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]}
result = self.client.search(
index=f"{self.index_prefix}_{index_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"]))
for batch in self._create_batches(items):
actions = [
{
"index": {
"_id": item["id"],
"_source": {
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
}
for item in batch
]
self.client.bulk(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"]))
for batch in self._create_batches(items):
actions = [
{
"index": {
"_id": item["id"],
"_source": {
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
}
for item in batch
]
self.client.bulk(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 reset(self):
indices = self.client.indices.get(index=f"{self.index_prefix}_*")
for index in indices:
self.client.indices.delete(index=index)
@@ -0,0 +1,354 @@
from typing import Optional, List, Dict, Any
from sqlalchemy import (
cast,
column,
create_engine,
Column,
Integer,
select,
text,
Text,
values,
)
from sqlalchemy.sql import true
from sqlalchemy.pool import NullPool
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
from sqlalchemy.dialects.postgresql import JSONB, array
from pgvector.sqlalchemy import Vector
from sqlalchemy.ext.mutable import MutableDict
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import PGVECTOR_DB_URL
VECTOR_LENGTH = 1536
Base = declarative_base()
class DocumentChunk(Base):
__tablename__ = "document_chunk"
id = Column(Text, primary_key=True)
vector = Column(Vector(dim=VECTOR_LENGTH), nullable=True)
collection_name = Column(Text, nullable=False)
text = Column(Text, nullable=True)
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
class PgvectorClient:
def __init__(self) -> None:
# if no pgvector uri, use the existing database connection
if not PGVECTOR_DB_URL:
from open_webui.apps.webui.internal.db import Session
self.session = Session
else:
engine = create_engine(
PGVECTOR_DB_URL, pool_pre_ping=True, poolclass=NullPool
)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
self.session = scoped_session(SessionLocal)
try:
# Ensure the pgvector extension is available
self.session.execute(text("CREATE EXTENSION IF NOT EXISTS vector;"))
# Create the tables if they do not exist
# Base.metadata.create_all requires a bind (engine or connection)
# Get the connection from the session
connection = self.session.connection()
Base.metadata.create_all(bind=connection)
# Create an index on the vector column if it doesn't exist
self.session.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_document_chunk_vector "
"ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);"
)
)
self.session.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name "
"ON document_chunk (collection_name);"
)
)
self.session.commit()
print("Initialization complete.")
except Exception as e:
self.session.rollback()
print(f"Error during initialization: {e}")
raise
def adjust_vector_length(self, vector: List[float]) -> List[float]:
# Adjust vector to have length VECTOR_LENGTH
current_length = len(vector)
if current_length < VECTOR_LENGTH:
# 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}"
)
return vector
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
try:
new_items = []
for item in items:
vector = self.adjust_vector_length(item["vector"])
new_chunk = DocumentChunk(
id=item["id"],
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=item["metadata"],
)
new_items.append(new_chunk)
self.session.bulk_save_objects(new_items)
self.session.commit()
print(
f"Inserted {len(new_items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
print(f"Error during insert: {e}")
raise
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
try:
for item in items:
vector = self.adjust_vector_length(item["vector"])
existing = (
self.session.query(DocumentChunk)
.filter(DocumentChunk.id == item["id"])
.first()
)
if existing:
existing.vector = vector
existing.text = item["text"]
existing.vmetadata = item["metadata"]
existing.collection_name = (
collection_name # Update collection_name if necessary
)
else:
new_chunk = DocumentChunk(
id=item["id"],
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=item["metadata"],
)
self.session.add(new_chunk)
self.session.commit()
print(f"Upserted {len(items)} items into collection '{collection_name}'.")
except Exception as e:
self.session.rollback()
print(f"Error during upsert: {e}")
raise
def search(
self,
collection_name: str,
vectors: List[List[float]],
limit: Optional[int] = None,
) -> Optional[SearchResult]:
try:
if not vectors:
return None
# Adjust query vectors to VECTOR_LENGTH
vectors = [self.adjust_vector_length(vector) for vector in vectors]
num_queries = len(vectors)
def vector_expr(vector):
return cast(array(vector), Vector(VECTOR_LENGTH))
# Create the values for query vectors
qid_col = column("qid", Integer)
q_vector_col = column("q_vector", Vector(VECTOR_LENGTH))
query_vectors = (
values(qid_col, q_vector_col)
.data(
[(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]
)
.alias("query_vectors")
)
# Build the lateral subquery for each query vector
subq = (
select(
DocumentChunk.id,
DocumentChunk.text,
DocumentChunk.vmetadata,
(
DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)
).label("distance"),
)
.where(DocumentChunk.collection_name == collection_name)
.order_by(
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))
)
)
if limit is not None:
subq = subq.limit(limit)
subq = subq.lateral("result")
# Build the main query by joining query_vectors and the lateral subquery
stmt = (
select(
query_vectors.c.qid,
subq.c.id,
subq.c.text,
subq.c.vmetadata,
subq.c.distance,
)
.select_from(query_vectors)
.join(subq, true())
.order_by(query_vectors.c.qid, subq.c.distance)
)
result_proxy = self.session.execute(stmt)
results = result_proxy.all()
ids = [[] for _ in range(num_queries)]
distances = [[] for _ in range(num_queries)]
documents = [[] for _ in range(num_queries)]
metadatas = [[] for _ in range(num_queries)]
if not results:
return SearchResult(
ids=ids,
distances=distances,
documents=documents,
metadatas=metadatas,
)
for row in results:
qid = int(row.qid)
ids[qid].append(row.id)
distances[qid].append(row.distance)
documents[qid].append(row.text)
metadatas[qid].append(row.vmetadata)
return SearchResult(
ids=ids, distances=distances, documents=documents, metadatas=metadatas
)
except Exception as e:
print(f"Error during search: {e}")
return None
def query(
self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None
) -> Optional[GetResult]:
try:
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
for key, value in filter.items():
query = query.filter(DocumentChunk.vmetadata[key].astext == str(value))
if limit is not None:
query = query.limit(limit)
results = query.all()
if not results:
return None
ids = [[result.id for result in results]]
documents = [[result.text for result in results]]
metadatas = [[result.vmetadata for result in results]]
return GetResult(
ids=ids,
documents=documents,
metadatas=metadatas,
)
except Exception as e:
print(f"Error during query: {e}")
return None
def get(
self, collection_name: str, limit: Optional[int] = None
) -> Optional[GetResult]:
try:
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
if limit is not None:
query = query.limit(limit)
results = query.all()
if not results:
return None
ids = [[result.id for result in results]]
documents = [[result.text for result in results]]
metadatas = [[result.vmetadata for result in results]]
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
except Exception as e:
print(f"Error during get: {e}")
return None
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict[str, Any]] = None,
) -> None:
try:
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
if ids:
query = query.filter(DocumentChunk.id.in_(ids))
if filter:
for key, value in filter.items():
query = query.filter(
DocumentChunk.vmetadata[key].astext == str(value)
)
deleted = query.delete(synchronize_session=False)
self.session.commit()
print(f"Deleted {deleted} items from collection '{collection_name}'.")
except Exception as e:
self.session.rollback()
print(f"Error during delete: {e}")
raise
def reset(self) -> None:
try:
deleted = self.session.query(DocumentChunk).delete()
self.session.commit()
print(
f"Reset complete. Deleted {deleted} items from 'document_chunk' table."
)
except Exception as e:
self.session.rollback()
print(f"Error during reset: {e}")
raise
def close(self) -> None:
pass
def has_collection(self, collection_name: str) -> bool:
try:
exists = (
self.session.query(DocumentChunk)
.filter(DocumentChunk.collection_name == collection_name)
.first()
is not None
)
return exists
except Exception as e:
print(f"Error checking collection existence: {e}")
return False
def delete_collection(self, collection_name: str) -> None:
self.delete(collection_name)
print(f"Collection '{collection_name}' deleted.")
@@ -0,0 +1,184 @@
from typing import Optional
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
NO_LIMIT = 999999999
class QdrantClient:
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
)
def _result_to_get_result(self, points) -> GetResult:
ids = []
documents = []
metadatas = []
for point in points:
payload = point.payload
ids.append(point.id)
documents.append(payload["text"])
metadatas.append(payload["metadata"])
return GetResult(
**{
"ids": [ids],
"documents": [documents],
"metadatas": [metadatas],
}
)
def _create_collection(self, collection_name: str, dimension: int):
collection_name_with_prefix = f"{self.collection_prefix}_{collection_name}"
self.client.create_collection(
collection_name=collection_name_with_prefix,
vectors_config=models.VectorParams(
size=dimension, distance=models.Distance.COSINE
),
)
print(f"collection {collection_name_with_prefix} successfully created!")
def _create_collection_if_not_exists(self, collection_name, dimension):
if not self.has_collection(collection_name=collection_name):
self._create_collection(
collection_name=collection_name, dimension=dimension
)
def _create_points(self, items: list[VectorItem]):
return [
PointStruct(
id=item["id"],
vector=item["vector"],
payload={"text": item["text"], "metadata": item["metadata"]},
)
for item in items
]
def has_collection(self, collection_name: str) -> bool:
return self.client.collection_exists(
f"{self.collection_prefix}_{collection_name}"
)
def delete_collection(self, collection_name: str):
return self.client.delete_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
)
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
if limit is None:
limit = NO_LIMIT # otherwise qdrant would set limit to 10!
query_response = self.client.query_points(
collection_name=f"{self.collection_prefix}_{collection_name}",
query=vectors[0],
limit=limit,
)
get_result = self._result_to_get_result(query_response.points)
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
distances=[[point.score for point in query_response.points]],
)
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
# Construct the filter string for querying
if not self.has_collection(collection_name):
return None
try:
if limit is None:
limit = NO_LIMIT # otherwise qdrant would set limit to 10!
field_conditions = []
for key, value in filter.items():
field_conditions.append(
models.FieldCondition(
key=f"metadata.{key}", match=models.MatchValue(value=value)
)
)
points = self.client.query_points(
collection_name=f"{self.collection_prefix}_{collection_name}",
query_filter=models.Filter(should=field_conditions),
limit=limit,
)
return self._result_to_get_result(points.points)
except Exception as e:
print(e)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
points = self.client.query_points(
collection_name=f"{self.collection_prefix}_{collection_name}",
limit=NO_LIMIT, # otherwise qdrant would set limit to 10!
)
return self._result_to_get_result(points.points)
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.
self._create_collection_if_not_exists(collection_name, len(items[0]["vector"]))
points = self._create_points(items)
self.client.upload_points(f"{self.collection_prefix}_{collection_name}", points)
def upsert(self, collection_name: str, items: list[VectorItem]):
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
self._create_collection_if_not_exists(collection_name, len(items[0]["vector"]))
points = self._create_points(items)
return self.client.upsert(f"{self.collection_prefix}_{collection_name}", points)
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
field_conditions = []
if ids:
for id_value in ids:
field_conditions.append(
models.FieldCondition(
key="metadata.id",
match=models.MatchValue(value=id_value),
),
),
elif filter:
for key, value in filter.items():
field_conditions.append(
models.FieldCondition(
key=f"metadata.{key}",
match=models.MatchValue(value=value),
),
),
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
points_selector=models.FilterSelector(
filter=models.Filter(must=field_conditions)
),
)
def reset(self):
# Resets the database. This will delete all collections and item entries.
collection_names = self.client.get_collections().collections
for collection_name in collection_names:
if collection_name.name.startswith(self.collection_prefix):
self.client.delete_collection(collection_name=collection_name.name)
@@ -0,0 +1,73 @@
import logging
import os
from pprint import pprint
from typing import Optional
import requests
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
import argparse
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
"""
Documentation: https://docs.microsoft.com/en-us/bing/search-apis/bing-web-search/overview
"""
def search_bing(
subscription_key: str,
endpoint: str,
locale: str,
query: str,
count: int,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
mkt = locale
params = {"q": query, "mkt": mkt, "answerCount": count}
headers = {"Ocp-Apim-Subscription-Key": subscription_key}
try:
response = requests.get(endpoint, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = json_response.get("webPages", {}).get("value", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["url"],
title=result.get("name"),
snippet=result.get("snippet"),
)
for result in results
]
except Exception as ex:
log.error(f"Error: {ex}")
raise ex
def main():
parser = argparse.ArgumentParser(description="Search Bing from the command line.")
parser.add_argument(
"query",
type=str,
default="Top 10 international news today",
help="The search query.",
)
parser.add_argument(
"--count", type=int, default=10, help="Number of search results to return."
)
parser.add_argument(
"--filter", nargs="*", help="List of filters to apply to the search results."
)
parser.add_argument(
"--locale",
type=str,
default="en-US",
help="The locale to use for the search, maps to market in api",
)
args = parser.parse_args()
results = search_bing(args.locale, args.query, args.count, args.filter)
pprint(results)
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -1,7 +1,7 @@
import logging
from typing import Optional
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
from open_webui.env import SRC_LOG_LEVELS
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -1,7 +1,7 @@
import logging
import requests
from open_webui.apps.rag.search.main import SearchResult
from open_webui.apps.retrieval.web.main import SearchResult
from open_webui.env import SRC_LOG_LEVELS
from yarl import URL
@@ -9,7 +9,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_jina(query: str, count: int) -> list[SearchResult]:
def search_jina(api_key: str, query: str, count: int) -> list[SearchResult]:
"""
Search using Jina's Search API and return the results as a list of SearchResult objects.
Args:
@@ -20,9 +20,7 @@ def search_jina(query: str, count: int) -> list[SearchResult]:
list[SearchResult]: A list of search results
"""
jina_search_endpoint = "https://s.jina.ai/"
headers = {
"Accept": "application/json",
}
headers = {"Accept": "application/json", "Authorization": f"Bearer {api_key}"}
url = str(URL(jina_search_endpoint + query))
response = requests.get(url, headers=headers)
response.raise_for_status()
@@ -0,0 +1,40 @@
import logging
from typing import Optional
import requests
from open_webui.apps.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_mojeek(
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
) -> list[SearchResult]:
"""Search using Mojeek's Search API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Mojeek Search API key
query (str): The query to search for
"""
url = "https://api.mojeek.com/search"
headers = {
"Accept": "application/json",
}
params = {"q": query, "api_key": api_key, "fmt": "json", "t": count}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = json_response.get("response", {}).get("results", [])
print(results)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("desc")
)
for result in results
]
@@ -3,7 +3,7 @@ from typing import Optional
from urllib.parse import urlencode
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -3,7 +3,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -3,7 +3,7 @@ from typing import Optional
from urllib.parse import urlencode
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -1,7 +1,7 @@
import logging
import requests
from open_webui.apps.rag.search.main import SearchResult
from open_webui.apps.retrieval.web.main import SearchResult
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -0,0 +1,58 @@
{
"_type": "SearchResponse",
"queryContext": {
"originalQuery": "Top 10 international results"
},
"webPages": {
"webSearchUrl": "https://www.bing.com/search?q=Top+10+international+results",
"totalEstimatedMatches": 687,
"value": [
{
"id": "https://api.bing.microsoft.com/api/v7/#WebPages.0",
"name": "2024 Mexican Grand Prix - F1 results and latest standings ... - PlanetF1",
"url": "https://www.planetf1.com/news/f1-results-2024-mexican-grand-prix-race-standings",
"datePublished": "2024-10-27T00:00:00.0000000",
"datePublishedFreshnessText": "1 day ago",
"isFamilyFriendly": true,
"displayUrl": "https://www.planetf1.com/news/f1-results-2024-mexican-grand-prix-race-standings",
"snippet": "Nico Hulkenberg and Pierre Gasly completed the top 10. A full report of the Mexican Grand Prix is available at the bottom of this article. F1 results 2024 Mexican Grand Prix",
"dateLastCrawled": "2024-10-28T07:15:00.0000000Z",
"cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=916492551782&mkt=en-US&setlang=en-US&w=zBsfaAPyF2tUrHFHr_vFFdUm8sng4g34",
"language": "en",
"isNavigational": false,
"noCache": false
},
{
"id": "https://api.bing.microsoft.com/api/v7/#WebPages.1",
"name": "F1 Results Today: HUGE Verstappen penalties cause major title change",
"url": "https://www.gpfans.com/en/f1-news/1033512/f1-results-today-mexican-grand-prix-huge-max-verstappen-penalties-cause-major-title-change/",
"datePublished": "2024-10-27T00:00:00.0000000",
"datePublishedFreshnessText": "1 day ago",
"isFamilyFriendly": true,
"displayUrl": "https://www.gpfans.com/en/f1-news/1033512/f1-results-today-mexican-grand-prix-huge-max...",
"snippet": "Elsewhere, Mercedes duo Lewis Hamilton and George Russell came home in P4 and P5 respectively. Meanwhile, the surprise package of the day were Haas, with both Kevin Magnussen and Nico Hulkenberg finishing inside the points.. READ MORE: RB star issues apology after red flag CRASH at Mexican GP Mexican Grand Prix 2024 results. 1. Carlos Sainz [Ferrari] 2. Lando Norris [McLaren] - +4.705",
"dateLastCrawled": "2024-10-28T06:06:00.0000000Z",
"cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=2840656522642&mkt=en-US&setlang=en-US&w=-Tbkwxnq52jZCvG7l3CtgcwT1vwAjIUD",
"language": "en",
"isNavigational": false,
"noCache": false
},
{
"id": "https://api.bing.microsoft.com/api/v7/#WebPages.2",
"name": "International Power Rankings: England flying, Kangaroos cruising, Fiji rise",
"url": "https://www.loverugbyleague.com/post/international-power-rankings-england-flying-kangaroos-cruising-fiji-rise",
"datePublished": "2024-10-28T00:00:00.0000000",
"datePublishedFreshnessText": "7 hours ago",
"isFamilyFriendly": true,
"displayUrl": "https://www.loverugbyleague.com/post/international-power-rankings-england-flying...",
"snippet": "LRL RECOMMENDS: England player ratings from first Test against Samoa as omnificent George Williams scores perfect 10. 2. Australia (Men) SAME. The Kangaroos remain 2nd in our Power Rankings after their 22-10 win against New Zealand in Christchurch on Sunday. As was the case in their win against Tonga last week, Mal Meningas side weren ...",
"dateLastCrawled": "2024-10-28T07:09:00.0000000Z",
"cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=1535008462672&mkt=en-US&setlang=en-US&w=82ujhH4Kp0iuhCS7wh1xLUFYUeetaVVm",
"language": "en",
"isNavigational": false,
"noCache": false
}
],
"someResultsRemoved": true
}
}
@@ -0,0 +1,97 @@
import socket
import urllib.parse
import validators
from typing import Union, Sequence, Iterator
from langchain_community.document_loaders import (
WebBaseLoader,
)
from langchain_core.documents import Document
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import ENABLE_RAG_LOCAL_WEB_FETCH
from open_webui.env import SRC_LOG_LEVELS
import logging
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def validate_url(url: Union[str, Sequence[str]]):
if isinstance(url, str):
if isinstance(validators.url(url), validators.ValidationError):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
# Check if any of the resolved addresses are private
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
for ip in ipv4_addresses:
if validators.ipv4(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
for ip in ipv6_addresses:
if validators.ipv6(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return True
elif isinstance(url, Sequence):
return all(validate_url(u) for u in url)
else:
return False
def resolve_hostname(hostname):
# Get address information
addr_info = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
return ipv4_addresses, ipv6_addresses
class SafeWebBaseLoader(WebBaseLoader):
"""WebBaseLoader with enhanced error handling for URLs."""
def lazy_load(self) -> Iterator[Document]:
"""Lazy load text from the url(s) in web_path with error handling."""
for path in self.web_paths:
try:
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
text = soup.get_text(**self.bs_get_text_kwargs)
# Build metadata
metadata = {"source": path}
if title := soup.find("title"):
metadata["title"] = title.get_text()
if description := soup.find("meta", attrs={"name": "description"}):
metadata["description"] = description.get(
"content", "No description found."
)
if html := soup.find("html"):
metadata["language"] = html.get("lang", "No language found.")
yield Document(page_content=text, metadata=metadata)
except Exception as e:
# Log the error and continue with the next URL
log.error(f"Error loading {path}: {e}")
def get_web_loader(
url: Union[str, Sequence[str]],
verify_ssl: bool = True,
requests_per_second: int = 2,
):
# Check if the URL is valid
if not validate_url(url):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return SafeWebBaseLoader(
url,
verify_ssl=verify_ssl,
requests_per_second=requests_per_second,
continue_on_failure=True,
)
+6 -4
View File
@@ -1,3 +1,5 @@
# TODO: move socket to webui app
import asyncio
import socketio
import logging
@@ -48,8 +50,6 @@ else:
)
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
# Dictionary to maintain the user pool
if WEBSOCKET_MANAGER == "redis":
@@ -92,8 +92,10 @@ async def periodic_usage_pool_cleanup():
await asyncio.sleep(TIMEOUT_DURATION)
# Start the cleanup task when your app starts
asyncio.create_task(periodic_usage_pool_cleanup())
app = socketio.ASGIApp(
sio,
socketio_path="/ws/socket.io",
)
def get_models_in_use():
+24 -2
View File
@@ -4,11 +4,20 @@ from contextlib import contextmanager
from typing import Any, Optional
from open_webui.apps.webui.internal.wrappers import register_connection
from open_webui.env import OPEN_WEBUI_DIR, DATABASE_URL, SRC_LOG_LEVELS
from open_webui.env import (
OPEN_WEBUI_DIR,
DATABASE_URL,
SRC_LOG_LEVELS,
DATABASE_POOL_MAX_OVERFLOW,
DATABASE_POOL_RECYCLE,
DATABASE_POOL_SIZE,
DATABASE_POOL_TIMEOUT,
)
from peewee_migrate import Router
from sqlalchemy import Dialect, create_engine, types
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.pool import QueuePool, NullPool
from sqlalchemy.sql.type_api import _T
from typing_extensions import Self
@@ -71,7 +80,20 @@ if "sqlite" in SQLALCHEMY_DATABASE_URL:
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
if DATABASE_POOL_SIZE > 0:
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
pool_size=DATABASE_POOL_SIZE,
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
pool_timeout=DATABASE_POOL_TIMEOUT,
pool_recycle=DATABASE_POOL_RECYCLE,
pool_pre_ping=True,
poolclass=QueuePool,
)
else:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
)
SessionLocal = sessionmaker(
+128 -12
View File
@@ -1,6 +1,7 @@
import inspect
import json
import logging
import time
from typing import AsyncGenerator, Generator, Iterator
from open_webui.apps.socket.main import get_event_call, get_event_emitter
@@ -9,13 +10,16 @@ from open_webui.apps.webui.models.models import Models
from open_webui.apps.webui.routers import (
auths,
chats,
folders,
configs,
documents,
groups,
files,
functions,
memories,
models,
knowledge,
prompts,
evaluations,
tools,
users,
utils,
@@ -27,22 +31,45 @@ from open_webui.config import (
DEFAULT_MODELS,
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_USER_ROLE,
MODEL_ORDER_LIST,
ENABLE_COMMUNITY_SHARING,
ENABLE_LOGIN_FORM,
ENABLE_MESSAGE_RATING,
ENABLE_SIGNUP,
ENABLE_API_KEY,
ENABLE_EVALUATION_ARENA_MODELS,
EVALUATION_ARENA_MODELS,
DEFAULT_ARENA_MODEL,
JWT_EXPIRES_IN,
ENABLE_OAUTH_ROLE_MANAGEMENT,
OAUTH_ROLES_CLAIM,
OAUTH_EMAIL_CLAIM,
OAUTH_PICTURE_CLAIM,
OAUTH_USERNAME_CLAIM,
OAUTH_ALLOWED_ROLES,
OAUTH_ADMIN_ROLES,
SHOW_ADMIN_DETAILS,
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH,
WEBUI_BANNERS,
ENABLE_LDAP,
LDAP_SERVER_LABEL,
LDAP_SERVER_HOST,
LDAP_SERVER_PORT,
LDAP_ATTRIBUTE_FOR_USERNAME,
LDAP_SEARCH_FILTERS,
LDAP_SEARCH_BASE,
LDAP_APP_DN,
LDAP_APP_PASSWORD,
LDAP_USE_TLS,
LDAP_CA_CERT_FILE,
LDAP_CIPHERS,
AppConfig,
)
from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
)
@@ -62,14 +89,21 @@ from open_webui.utils.payload import (
from open_webui.utils.tools import get_tools
app = FastAPI()
app = FastAPI(
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
app.state.config = AppConfig()
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
app.state.config.ENABLE_API_KEY = ENABLE_API_KEY
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
@@ -82,18 +116,41 @@ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
app.state.MODELS = {}
app.state.config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
app.state.config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
app.state.config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES
app.state.config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
app.state.config.ENABLE_LDAP = ENABLE_LDAP
app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL
app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST
app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT
app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME
app.state.config.LDAP_APP_DN = LDAP_APP_DN
app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD
app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE
app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS
app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
app.state.TOOLS = {}
app.state.FUNCTIONS = {}
@@ -107,18 +164,25 @@ app.add_middleware(
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(models.router, prefix="/models", tags=["models"])
app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(tools.router, prefix="/tools", tags=["tools"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(folders.router, prefix="/folders", tags=["folders"])
app.include_router(groups.router, prefix="/groups", tags=["groups"])
app.include_router(files.router, prefix="/files", tags=["files"])
app.include_router(tools.router, prefix="/tools", tags=["tools"])
app.include_router(functions.router, prefix="/functions", tags=["functions"])
app.include_router(evaluations.router, prefix="/evaluations", tags=["evaluations"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@@ -133,6 +197,47 @@ async def get_status():
}
async def get_all_models():
models = []
pipe_models = await get_pipe_models()
models = models + pipe_models
if app.state.config.ENABLE_EVALUATION_ARENA_MODELS:
arena_models = []
if len(app.state.config.EVALUATION_ARENA_MODELS) > 0:
arena_models = [
{
"id": model["id"],
"name": model["name"],
"info": {
"meta": model["meta"],
},
"object": "model",
"created": int(time.time()),
"owned_by": "arena",
"arena": True,
}
for model in app.state.config.EVALUATION_ARENA_MODELS
]
else:
# Add default arena model
arena_models = [
{
"id": DEFAULT_ARENA_MODEL["id"],
"name": DEFAULT_ARENA_MODEL["name"],
"info": {
"meta": DEFAULT_ARENA_MODEL["meta"],
},
"object": "model",
"created": int(time.time()),
"owned_by": "arena",
"arena": True,
}
]
models = models + arena_models
return models
def get_function_module(pipe_id: str):
# Check if function is already loaded
if pipe_id not in app.state.FUNCTIONS:
@@ -169,7 +274,9 @@ async def get_pipe_models():
log.exception(e)
sub_pipes = []
print(sub_pipes)
log.debug(
f"get_pipe_models: function '{pipe.id}' is a manifold of {sub_pipes}"
)
for p in sub_pipes:
sub_pipe_id = f'{pipe.id}.{p["id"]}'
@@ -179,6 +286,7 @@ async def get_pipe_models():
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
pipe_flag = {"type": pipe.type}
pipe_models.append(
{
"id": sub_pipe_id,
@@ -192,6 +300,10 @@ async def get_pipe_models():
else:
pipe_flag = {"type": "pipe"}
log.debug(
f"get_pipe_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
)
pipe_models.append(
{
"id": pipe.id,
@@ -245,7 +357,7 @@ def get_pipe_id(form_data: dict) -> str:
pipe_id = form_data["model"]
if "." in pipe_id:
pipe_id, _ = pipe_id.split(".", 1)
print(pipe_id)
return pipe_id
@@ -272,7 +384,7 @@ def get_function_params(function_module, form_data, user, extra_params=None):
return params
async def generate_function_chat_completion(form_data, user):
async def generate_function_chat_completion(form_data, user, models: dict = {}):
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
@@ -287,17 +399,20 @@ async def generate_function_chat_completion(form_data, user):
__event_emitter__ = None
__event_call__ = None
__task__ = None
__task_body__ = None
if metadata:
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
__event_emitter__ = get_event_emitter(metadata)
__event_call__ = get_event_call(metadata)
__task__ = metadata.get("task", None)
__task_body__ = metadata.get("task_body", None)
extra_params = {
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
"__task__": __task__,
"__task_body__": __task_body__,
"__files__": files,
"__user__": {
"id": user.id,
@@ -305,6 +420,7 @@ async def generate_function_chat_completion(form_data, user):
"name": user.name,
"role": user.role,
},
"__metadata__": metadata,
}
extra_params["__tools__"] = get_tools(
app,
@@ -312,7 +428,7 @@ async def generate_function_chat_completion(form_data, user):
user,
{
**extra_params,
"__model__": app.state.MODELS[form_data["model"]],
"__model__": models.get(form_data["model"], None),
"__messages__": form_data["messages"],
"__files__": files,
},
@@ -332,7 +448,7 @@ async def generate_function_chat_completion(form_data, user):
pipe = function_module.pipe
params = get_function_params(function_module, form_data, user, extra_params)
if form_data["stream"]:
if form_data.get("stream", False):
async def stream_content():
try:
@@ -348,7 +464,7 @@ async def generate_function_chat_completion(form_data, user):
return
except Exception as e:
print(f"Error: {e}")
log.error(f"Error: {e}")
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
return
@@ -378,7 +494,7 @@ async def generate_function_chat_completion(form_data, user):
res = await execute_pipe(pipe, params)
except Exception as e:
print(f"Error: {e}")
log.error(f"Error: {e}")
return {"error": {"detail": str(e)}}
if isinstance(res, StreamingResponse) or isinstance(res, dict):
@@ -64,6 +64,11 @@ class SigninForm(BaseModel):
password: str
class LdapForm(BaseModel):
user: str
password: str
class ProfileImageUrlForm(BaseModel):
profile_image_url: str
+452 -26
View File
@@ -4,8 +4,13 @@ import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.apps.webui.models.tags import TagModel, Tag, Tags
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from sqlalchemy import or_, func, select, and_, text
from sqlalchemy.sql import exists
####################
# Chat DB Schema
@@ -18,13 +23,17 @@ class Chat(Base):
id = Column(String, primary_key=True)
user_id = Column(String)
title = Column(Text)
chat = Column(Text) # Save Chat JSON as Text
chat = Column(JSON)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
share_id = Column(Text, unique=True, nullable=True)
archived = Column(Boolean, default=False)
pinned = Column(Boolean, default=False, nullable=True)
meta = Column(JSON, server_default="{}")
folder_id = Column(Text, nullable=True)
class ChatModel(BaseModel):
@@ -33,13 +42,17 @@ class ChatModel(BaseModel):
id: str
user_id: str
title: str
chat: str
chat: dict
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
share_id: Optional[str] = None
archived: bool = False
pinned: Optional[bool] = False
meta: dict = {}
folder_id: Optional[str] = None
####################
@@ -51,6 +64,17 @@ class ChatForm(BaseModel):
chat: dict
class ChatImportForm(ChatForm):
meta: Optional[dict] = {}
pinned: Optional[bool] = False
folder_id: Optional[str] = None
class ChatTitleMessagesForm(BaseModel):
title: str
messages: list[dict]
class ChatTitleForm(BaseModel):
title: str
@@ -64,6 +88,9 @@ class ChatResponse(BaseModel):
created_at: int # timestamp in epoch
share_id: Optional[str] = None # id of the chat to be shared
archived: bool
pinned: Optional[bool] = False
meta: dict = {}
folder_id: Optional[str] = None
class ChatTitleIdResponse(BaseModel):
@@ -86,7 +113,36 @@ class ChatTable:
if "title" in form_data.chat
else "New Chat"
),
"chat": json.dumps(form_data.chat),
"chat": form_data.chat,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Chat(**chat.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return ChatModel.model_validate(result) if result else None
def import_chat(
self, user_id: str, form_data: ChatImportForm
) -> Optional[ChatModel]:
with get_db() as db:
id = str(uuid.uuid4())
chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"]
if "title" in form_data.chat
else "New Chat"
),
"chat": form_data.chat,
"meta": form_data.meta,
"pinned": form_data.pinned,
"folder_id": form_data.folder_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
@@ -101,14 +157,14 @@ class ChatTable:
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
try:
with get_db() as db:
chat_obj = db.get(Chat, id)
chat_obj.chat = json.dumps(chat)
chat_obj.title = chat["title"] if "title" in chat else "New Chat"
chat_obj.updated_at = int(time.time())
chat_item = db.get(Chat, id)
chat_item.chat = chat
chat_item.title = chat["title"] if "title" in chat else "New Chat"
chat_item.updated_at = int(time.time())
db.commit()
db.refresh(chat_obj)
db.refresh(chat_item)
return ChatModel.model_validate(chat_obj)
return ChatModel.model_validate(chat_item)
except Exception:
return None
@@ -147,15 +203,22 @@ class ChatTable:
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
print("update_shared_chat_by_id")
chat = db.get(Chat, chat_id)
print(chat)
chat.title = chat.title
chat.chat = chat.chat
db.commit()
db.refresh(chat)
shared_chat = (
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").first()
)
return self.get_chat_by_id(chat.share_id)
if shared_chat is None:
return self.insert_shared_chat_by_chat_id(chat_id)
shared_chat.title = chat.title
shared_chat.chat = chat.chat
shared_chat.updated_at = int(time.time())
db.commit()
db.refresh(shared_chat)
return ChatModel.model_validate(shared_chat)
except Exception:
return None
@@ -182,11 +245,24 @@ class ChatTable:
except Exception:
return None
def toggle_chat_pinned_by_id(self, id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.get(Chat, id)
chat.pinned = not chat.pinned
chat.updated_at = int(time.time())
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except Exception:
return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.get(Chat, id)
chat.archived = not chat.archived
chat.updated_at = int(time.time())
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
@@ -223,14 +299,18 @@ class ChatTable:
limit: int = 50,
) -> list[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id)
query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
if not include_archived:
query = query.filter_by(archived=False)
all_chats = (
query.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
.all()
)
query = query.order_by(Chat.updated_at.desc())
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
all_chats = query.all()
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_title_id_list_by_user_id(
@@ -241,7 +321,9 @@ class ChatTable:
limit: Optional[int] = None,
) -> list[ChatTitleIdResponse]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id)
query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
if not include_archived:
query = query.filter_by(archived=False)
@@ -249,10 +331,10 @@ class ChatTable:
Chat.id, Chat.title, Chat.updated_at, Chat.created_at
)
if limit:
query = query.limit(limit)
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
all_chats = query.all()
@@ -328,6 +410,15 @@ class ChatTable:
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_pinned_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id, pinned=True, archived=False)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
with get_db() as db:
all_chats = (
@@ -337,6 +428,329 @@ class ChatTable:
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chats_by_user_id_and_search_text(
self,
user_id: str,
search_text: str,
include_archived: bool = False,
skip: int = 0,
limit: int = 60,
) -> list[ChatModel]:
"""
Filters chats based on a search query using Python, allowing pagination using skip and limit.
"""
search_text = search_text.lower().strip()
if not search_text:
return self.get_chat_list_by_user_id(user_id, include_archived, skip, limit)
search_text_words = search_text.split(" ")
# search_text might contain 'tag:tag_name' format so we need to extract the tag_name, split the search_text and remove the tags
tag_ids = [
word.replace("tag:", "").replace(" ", "_").lower()
for word in search_text_words
if word.startswith("tag:")
]
search_text_words = [
word for word in search_text_words if not word.startswith("tag:")
]
search_text = " ".join(search_text_words)
with get_db() as db:
query = db.query(Chat).filter(Chat.user_id == user_id)
if not include_archived:
query = query.filter(Chat.archived == False)
query = query.order_by(Chat.updated_at.desc())
# Check if the database dialect is either 'sqlite' or 'postgresql'
dialect_name = db.bind.dialect.name
if dialect_name == "sqlite":
# SQLite case: using JSON1 extension for JSON searching
query = query.filter(
(
Chat.title.ilike(
f"%{search_text}%"
) # Case-insensitive search in title
| text(
"""
EXISTS (
SELECT 1
FROM json_each(Chat.chat, '$.messages') AS message
WHERE LOWER(message.value->>'content') LIKE '%' || :search_text || '%'
)
"""
)
).params(search_text=search_text)
)
# Check if there are any tags to filter, it should have all the tags
if "none" in tag_ids:
query = query.filter(
text(
"""
NOT EXISTS (
SELECT 1
FROM json_each(Chat.meta, '$.tags') AS tag
)
"""
)
)
elif tag_ids:
query = query.filter(
and_(
*[
text(
f"""
EXISTS (
SELECT 1
FROM json_each(Chat.meta, '$.tags') AS tag
WHERE tag.value = :tag_id_{tag_idx}
)
"""
).params(**{f"tag_id_{tag_idx}": tag_id})
for tag_idx, tag_id in enumerate(tag_ids)
]
)
)
elif dialect_name == "postgresql":
# PostgreSQL relies on proper JSON query for search
query = query.filter(
(
Chat.title.ilike(
f"%{search_text}%"
) # Case-insensitive search in title
| text(
"""
EXISTS (
SELECT 1
FROM json_array_elements(Chat.chat->'messages') AS message
WHERE LOWER(message->>'content') LIKE '%' || :search_text || '%'
)
"""
)
).params(search_text=search_text)
)
# Check if there are any tags to filter, it should have all the tags
if "none" in tag_ids:
query = query.filter(
text(
"""
NOT EXISTS (
SELECT 1
FROM json_array_elements_text(Chat.meta->'tags') AS tag
)
"""
)
)
elif tag_ids:
query = query.filter(
and_(
*[
text(
f"""
EXISTS (
SELECT 1
FROM json_array_elements_text(Chat.meta->'tags') AS tag
WHERE tag = :tag_id_{tag_idx}
)
"""
).params(**{f"tag_id_{tag_idx}": tag_id})
for tag_idx, tag_id in enumerate(tag_ids)
]
)
)
else:
raise NotImplementedError(
f"Unsupported dialect: {db.bind.dialect.name}"
)
# Perform pagination at the SQL level
all_chats = query.offset(skip).limit(limit).all()
print(len(all_chats))
# Validate and return chats
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chats_by_folder_id_and_user_id(
self, folder_id: str, user_id: str
) -> list[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id)
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
query = query.filter_by(archived=False)
query = query.order_by(Chat.updated_at.desc())
all_chats = query.all()
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chats_by_folder_ids_and_user_id(
self, folder_ids: list[str], user_id: str
) -> list[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter(
Chat.folder_id.in_(folder_ids), Chat.user_id == user_id
)
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
query = query.filter_by(archived=False)
query = query.order_by(Chat.updated_at.desc())
all_chats = query.all()
return [ChatModel.model_validate(chat) for chat in all_chats]
def update_chat_folder_id_by_id_and_user_id(
self, id: str, user_id: str, folder_id: str
) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.get(Chat, id)
chat.folder_id = folder_id
chat.updated_at = int(time.time())
chat.pinned = False
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except Exception:
return None
def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]:
with get_db() as db:
chat = db.get(Chat, id)
tags = chat.meta.get("tags", [])
return [Tags.get_tag_by_name_and_user_id(tag, user_id) for tag in tags]
def get_chat_list_by_user_id_and_tag_name(
self, user_id: str, tag_name: str, skip: int = 0, limit: int = 50
) -> list[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id)
tag_id = tag_name.replace(" ", "_").lower()
print(db.bind.dialect.name)
if db.bind.dialect.name == "sqlite":
# SQLite JSON1 querying for tags within the meta JSON field
query = query.filter(
text(
f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)"
)
).params(tag_id=tag_id)
elif db.bind.dialect.name == "postgresql":
# PostgreSQL JSON query for tags within the meta JSON field (for `json` type)
query = query.filter(
text(
"EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)"
)
).params(tag_id=tag_id)
else:
raise NotImplementedError(
f"Unsupported dialect: {db.bind.dialect.name}"
)
all_chats = query.all()
print("all_chats", all_chats)
return [ChatModel.model_validate(chat) for chat in all_chats]
def add_chat_tag_by_id_and_user_id_and_tag_name(
self, id: str, user_id: str, tag_name: str
) -> Optional[ChatModel]:
tag = Tags.get_tag_by_name_and_user_id(tag_name, user_id)
if tag is None:
tag = Tags.insert_new_tag(tag_name, user_id)
try:
with get_db() as db:
chat = db.get(Chat, id)
tag_id = tag.id
if tag_id not in chat.meta.get("tags", []):
chat.meta = {
**chat.meta,
"tags": list(set(chat.meta.get("tags", []) + [tag_id])),
}
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except Exception:
return None
def count_chats_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> int:
with get_db() as db: # Assuming `get_db()` returns a session object
query = db.query(Chat).filter_by(user_id=user_id, archived=False)
# Normalize the tag_name for consistency
tag_id = tag_name.replace(" ", "_").lower()
if db.bind.dialect.name == "sqlite":
# SQLite JSON1 support for querying the tags inside the `meta` JSON field
query = query.filter(
text(
f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)"
)
).params(tag_id=tag_id)
elif db.bind.dialect.name == "postgresql":
# PostgreSQL JSONB support for querying the tags inside the `meta` JSON field
query = query.filter(
text(
"EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)"
)
).params(tag_id=tag_id)
else:
raise NotImplementedError(
f"Unsupported dialect: {db.bind.dialect.name}"
)
# Get the count of matching records
count = query.count()
# Debugging output for inspection
print(f"Count of chats for tag '{tag_name}':", count)
return count
def delete_tag_by_id_and_user_id_and_tag_name(
self, id: str, user_id: str, tag_name: str
) -> bool:
try:
with get_db() as db:
chat = db.get(Chat, id)
tags = chat.meta.get("tags", [])
tag_id = tag_name.replace(" ", "_").lower()
tags = [tag for tag in tags if tag != tag_id]
chat.meta = {
**chat.meta,
"tags": list(set(tags)),
}
db.commit()
return True
except Exception:
return False
def delete_all_tags_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
with get_db() as db:
chat = db.get(Chat, id)
chat.meta = {
**chat.meta,
"tags": [],
}
db.commit()
return True
except Exception:
return False
def delete_chat_by_id(self, id: str) -> bool:
try:
with get_db() as db:
@@ -369,6 +783,18 @@ class ChatTable:
except Exception:
return False
def delete_chats_by_user_id_and_folder_id(
self, user_id: str, folder_id: str
) -> bool:
try:
with get_db() as db:
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
db.commit()
return True
except Exception:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
with get_db() as db:
@@ -1,157 +0,0 @@
import json
import logging
import time
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Documents DB Schema
####################
class Document(Base):
__tablename__ = "document"
collection_name = Column(String, primary_key=True)
name = Column(String, unique=True)
title = Column(Text)
filename = Column(Text)
content = Column(Text, nullable=True)
user_id = Column(String)
timestamp = Column(BigInteger)
class DocumentModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
collection_name: str
name: str
title: str
filename: str
content: Optional[str] = None
user_id: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class DocumentResponse(BaseModel):
collection_name: str
name: str
title: str
filename: str
content: Optional[dict] = None
user_id: str
timestamp: int # timestamp in epoch
class DocumentUpdateForm(BaseModel):
name: str
title: str
class DocumentForm(DocumentUpdateForm):
collection_name: str
filename: str
content: Optional[str] = None
class DocumentsTable:
def insert_new_doc(
self, user_id: str, form_data: DocumentForm
) -> Optional[DocumentModel]:
with get_db() as db:
document = DocumentModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"timestamp": int(time.time()),
}
)
try:
result = Document(**document.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return DocumentModel.model_validate(result)
else:
return None
except Exception:
return None
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
try:
with get_db() as db:
document = db.query(Document).filter_by(name=name).first()
return DocumentModel.model_validate(document) if document else None
except Exception:
return None
def get_docs(self) -> list[DocumentModel]:
with get_db() as db:
return [
DocumentModel.model_validate(doc) for doc in db.query(Document).all()
]
def update_doc_by_name(
self, name: str, form_data: DocumentUpdateForm
) -> Optional[DocumentModel]:
try:
with get_db() as db:
db.query(Document).filter_by(name=name).update(
{
"title": form_data.title,
"name": form_data.name,
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(form_data.name)
except Exception as e:
log.exception(e)
return None
def update_doc_content_by_name(
self, name: str, updated: dict
) -> Optional[DocumentModel]:
try:
doc = self.get_doc_by_name(name)
doc_content = json.loads(doc.content if doc.content else "{}")
doc_content = {**doc_content, **updated}
with get_db() as db:
db.query(Document).filter_by(name=name).update(
{
"content": json.dumps(doc_content),
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(name)
except Exception as e:
log.exception(e)
return None
def delete_doc_by_name(self, name: str) -> bool:
try:
with get_db() as db:
db.query(Document).filter_by(name=name).delete()
db.commit()
return True
except Exception:
return False
Documents = DocumentsTable()
@@ -0,0 +1,254 @@
import logging
import time
import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.apps.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
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Feedback DB Schema
####################
class Feedback(Base):
__tablename__ = "feedback"
id = Column(Text, primary_key=True)
user_id = Column(Text)
version = Column(BigInteger, default=0)
type = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
snapshot = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class FeedbackModel(BaseModel):
id: str
user_id: str
version: int
type: str
data: Optional[dict] = None
meta: Optional[dict] = None
snapshot: Optional[dict] = None
created_at: int
updated_at: int
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FeedbackResponse(BaseModel):
id: str
user_id: str
version: int
type: str
data: Optional[dict] = None
meta: Optional[dict] = None
created_at: int
updated_at: int
class RatingData(BaseModel):
rating: Optional[str | int] = None
model_id: Optional[str] = None
sibling_model_ids: Optional[list[str]] = None
reason: Optional[str] = None
comment: Optional[str] = None
model_config = ConfigDict(extra="allow", protected_namespaces=())
class MetaData(BaseModel):
arena: Optional[bool] = None
chat_id: Optional[str] = None
message_id: Optional[str] = None
tags: Optional[list[str]] = None
model_config = ConfigDict(extra="allow")
class SnapshotData(BaseModel):
chat: Optional[dict] = None
model_config = ConfigDict(extra="allow")
class FeedbackForm(BaseModel):
type: str
data: Optional[RatingData] = None
meta: Optional[dict] = None
snapshot: Optional[SnapshotData] = None
model_config = ConfigDict(extra="allow")
class FeedbackTable:
def insert_new_feedback(
self, user_id: str, form_data: FeedbackForm
) -> Optional[FeedbackModel]:
with get_db() as db:
id = str(uuid.uuid4())
feedback = FeedbackModel(
**{
"id": id,
"user_id": user_id,
"version": 0,
**form_data.model_dump(),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
result = Feedback(**feedback.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FeedbackModel.model_validate(result)
else:
return None
except Exception as e:
print(e)
return None
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
try:
with get_db() as db:
feedback = db.query(Feedback).filter_by(id=id).first()
if not feedback:
return None
return FeedbackModel.model_validate(feedback)
except Exception:
return None
def get_feedback_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[FeedbackModel]:
try:
with get_db() as db:
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
if not feedback:
return None
return FeedbackModel.model_validate(feedback)
except Exception:
return None
def get_all_feedbacks(self) -> list[FeedbackModel]:
with get_db() as db:
return [
FeedbackModel.model_validate(feedback)
for feedback in db.query(Feedback)
.order_by(Feedback.updated_at.desc())
.all()
]
def get_feedbacks_by_type(self, type: str) -> list[FeedbackModel]:
with get_db() as db:
return [
FeedbackModel.model_validate(feedback)
for feedback in db.query(Feedback)
.filter_by(type=type)
.order_by(Feedback.updated_at.desc())
.all()
]
def get_feedbacks_by_user_id(self, user_id: str) -> list[FeedbackModel]:
with get_db() as db:
return [
FeedbackModel.model_validate(feedback)
for feedback in db.query(Feedback)
.filter_by(user_id=user_id)
.order_by(Feedback.updated_at.desc())
.all()
]
def update_feedback_by_id(
self, id: str, form_data: FeedbackForm
) -> Optional[FeedbackModel]:
with get_db() as db:
feedback = db.query(Feedback).filter_by(id=id).first()
if not feedback:
return None
if form_data.data:
feedback.data = form_data.data.model_dump()
if form_data.meta:
feedback.meta = form_data.meta
if form_data.snapshot:
feedback.snapshot = form_data.snapshot.model_dump()
feedback.updated_at = int(time.time())
db.commit()
return FeedbackModel.model_validate(feedback)
def update_feedback_by_id_and_user_id(
self, id: str, user_id: str, form_data: FeedbackForm
) -> Optional[FeedbackModel]:
with get_db() as db:
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
if not feedback:
return None
if form_data.data:
feedback.data = form_data.data.model_dump()
if form_data.meta:
feedback.meta = form_data.meta
if form_data.snapshot:
feedback.snapshot = form_data.snapshot.model_dump()
feedback.updated_at = int(time.time())
db.commit()
return FeedbackModel.model_validate(feedback)
def delete_feedback_by_id(self, id: str) -> bool:
with get_db() as db:
feedback = db.query(Feedback).filter_by(id=id).first()
if not feedback:
return False
db.delete(feedback)
db.commit()
return True
def delete_feedback_by_id_and_user_id(self, id: str, user_id: str) -> bool:
with get_db() as db:
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
if not feedback:
return False
db.delete(feedback)
db.commit()
return True
def delete_feedbacks_by_user_id(self, user_id: str) -> bool:
with get_db() as db:
feedbacks = db.query(Feedback).filter_by(user_id=user_id).all()
if not feedbacks:
return False
for feedback in feedbacks:
db.delete(feedback)
db.commit()
return True
def delete_all_feedbacks(self) -> bool:
with get_db() as db:
feedbacks = db.query(Feedback).all()
if not feedbacks:
return False
for feedback in feedbacks:
db.delete(feedback)
db.commit()
return True
Feedbacks = FeedbackTable()
+116 -7
View File
@@ -5,7 +5,7 @@ from typing import Optional
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import BigInteger, Column, String, Text, JSON
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -17,22 +17,35 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class File(Base):
__tablename__ = "file"
id = Column(String, primary_key=True)
user_id = Column(String)
hash = Column(Text, nullable=True)
filename = Column(Text)
meta = Column(JSONField)
path = Column(Text, nullable=True)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class FileModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
filename: str
meta: dict
created_at: int # timestamp in epoch
hash: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
filename: str
path: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
created_at: Optional[int] # timestamp in epoch
updated_at: Optional[int] # timestamp in epoch
####################
@@ -40,17 +53,42 @@ class FileModel(BaseModel):
####################
class FileMeta(BaseModel):
name: Optional[str] = None
content_type: Optional[str] = None
size: Optional[int] = None
model_config = ConfigDict(extra="allow")
class FileModelResponse(BaseModel):
id: str
user_id: str
hash: Optional[str] = None
filename: str
data: Optional[dict] = None
meta: FileMeta
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
model_config = ConfigDict(extra="allow")
class FileMetadataResponse(BaseModel):
id: str
meta: dict
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
class FileForm(BaseModel):
id: str
hash: Optional[str] = None
filename: str
path: str
data: dict = {}
meta: dict = {}
@@ -62,6 +100,7 @@ class FilesTable:
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
@@ -86,10 +125,48 @@ class FilesTable:
except Exception:
return None
def get_file_metadata_by_id(self, id: str) -> Optional[FileMetadataResponse]:
with get_db() as db:
try:
file = db.get(File, id)
return FileMetadataResponse(
id=file.id,
meta=file.meta,
created_at=file.created_at,
updated_at=file.updated_at,
)
except Exception:
return None
def get_files(self) -> list[FileModel]:
with get_db() as db:
return [FileModel.model_validate(file) for file in db.query(File).all()]
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
with get_db() as db:
return [
FileModel.model_validate(file)
for file in db.query(File)
.filter(File.id.in_(ids))
.order_by(File.updated_at.desc())
.all()
]
def get_file_metadatas_by_ids(self, ids: list[str]) -> list[FileMetadataResponse]:
with get_db() as db:
return [
FileMetadataResponse(
id=file.id,
meta=file.meta,
created_at=file.created_at,
updated_at=file.updated_at,
)
for file in db.query(File)
.filter(File.id.in_(ids))
.order_by(File.updated_at.desc())
.all()
]
def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
with get_db() as db:
return [
@@ -97,6 +174,38 @@ class FilesTable:
for file in db.query(File).filter_by(user_id=user_id).all()
]
def update_file_hash_by_id(self, id: str, hash: str) -> Optional[FileModel]:
with get_db() as db:
try:
file = db.query(File).filter_by(id=id).first()
file.hash = hash
db.commit()
return FileModel.model_validate(file)
except Exception:
return None
def update_file_data_by_id(self, id: str, data: dict) -> Optional[FileModel]:
with get_db() as db:
try:
file = db.query(File).filter_by(id=id).first()
file.data = {**(file.data if file.data else {}), **data}
db.commit()
return FileModel.model_validate(file)
except Exception as e:
return None
def update_file_metadata_by_id(self, id: str, meta: dict) -> Optional[FileModel]:
with get_db() as db:
try:
file = db.query(File).filter_by(id=id).first()
file.meta = {**(file.meta if file.meta else {}), **meta}
db.commit()
return FileModel.model_validate(file)
except Exception:
return None
def delete_file_by_id(self, id: str) -> bool:
with get_db() as db:
try:
@@ -0,0 +1,271 @@
import logging
import time
import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.apps.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
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Folder DB Schema
####################
class Folder(Base):
__tablename__ = "folder"
id = Column(Text, primary_key=True)
parent_id = Column(Text, nullable=True)
user_id = Column(Text)
name = Column(Text)
items = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
is_expanded = Column(Boolean, default=False)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class FolderModel(BaseModel):
id: str
parent_id: Optional[str] = None
user_id: str
name: str
items: Optional[dict] = None
meta: Optional[dict] = None
is_expanded: bool = False
created_at: int
updated_at: int
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FolderForm(BaseModel):
name: str
model_config = ConfigDict(extra="allow")
class FolderTable:
def insert_new_folder(
self, user_id: str, name: str, parent_id: Optional[str] = None
) -> Optional[FolderModel]:
with get_db() as db:
id = str(uuid.uuid4())
folder = FolderModel(
**{
"id": id,
"user_id": user_id,
"name": name,
"parent_id": parent_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
result = Folder(**folder.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FolderModel.model_validate(result)
else:
return None
except Exception as e:
print(e)
return None
def get_folder_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[FolderModel]:
try:
with get_db() as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return None
return FolderModel.model_validate(folder)
except Exception:
return None
def get_children_folders_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[FolderModel]:
try:
with get_db() as db:
folders = []
def get_children(folder):
children = self.get_folders_by_parent_id_and_user_id(
folder.id, user_id
)
for child in children:
get_children(child)
folders.append(child)
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return None
get_children(folder)
return folders
except Exception:
return None
def get_folders_by_user_id(self, user_id: str) -> list[FolderModel]:
with get_db() as db:
return [
FolderModel.model_validate(folder)
for folder in db.query(Folder).filter_by(user_id=user_id).all()
]
def get_folder_by_parent_id_and_user_id_and_name(
self, parent_id: Optional[str], user_id: str, name: str
) -> Optional[FolderModel]:
try:
with get_db() as db:
# Check if folder exists
folder = (
db.query(Folder)
.filter_by(parent_id=parent_id, user_id=user_id)
.filter(Folder.name.ilike(name))
.first()
)
if not folder:
return None
return FolderModel.model_validate(folder)
except Exception as e:
log.error(f"get_folder_by_parent_id_and_user_id_and_name: {e}")
return None
def get_folders_by_parent_id_and_user_id(
self, parent_id: Optional[str], user_id: str
) -> list[FolderModel]:
with get_db() as db:
return [
FolderModel.model_validate(folder)
for folder in db.query(Folder)
.filter_by(parent_id=parent_id, user_id=user_id)
.all()
]
def update_folder_parent_id_by_id_and_user_id(
self,
id: str,
user_id: str,
parent_id: str,
) -> Optional[FolderModel]:
try:
with get_db() as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return None
folder.parent_id = parent_id
folder.updated_at = int(time.time())
db.commit()
return FolderModel.model_validate(folder)
except Exception as e:
log.error(f"update_folder: {e}")
return
def update_folder_name_by_id_and_user_id(
self, id: str, user_id: str, name: str
) -> Optional[FolderModel]:
try:
with get_db() as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return None
existing_folder = (
db.query(Folder)
.filter_by(name=name, parent_id=folder.parent_id, user_id=user_id)
.first()
)
if existing_folder:
return None
folder.name = name
folder.updated_at = int(time.time())
db.commit()
return FolderModel.model_validate(folder)
except Exception as e:
log.error(f"update_folder: {e}")
return
def update_folder_is_expanded_by_id_and_user_id(
self, id: str, user_id: str, is_expanded: bool
) -> Optional[FolderModel]:
try:
with get_db() as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return None
folder.is_expanded = is_expanded
folder.updated_at = int(time.time())
db.commit()
return FolderModel.model_validate(folder)
except Exception as e:
log.error(f"update_folder: {e}")
return
def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> 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)
# Delete all children folders
def delete_children(folder):
folder_children = self.get_folders_by_parent_id_and_user_id(
folder.id, user_id
)
for folder_child in folder_children:
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()
db.delete(folder)
db.commit()
delete_children(folder)
db.delete(folder)
db.commit()
return True
except Exception as e:
log.error(f"delete_folder: {e}")
return False
Folders = FolderTable()
@@ -0,0 +1,186 @@
import json
import logging
import time
from typing import Optional
import uuid
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from open_webui.apps.webui.models.files import FileMetadataResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON, func
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# UserGroup DB Schema
####################
class Group(Base):
__tablename__ = "group"
id = Column(Text, unique=True, primary_key=True)
user_id = Column(Text)
name = Column(Text)
description = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
permissions = Column(JSON, nullable=True)
user_ids = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class GroupModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
name: str
description: str
data: Optional[dict] = None
meta: Optional[dict] = None
permissions: Optional[dict] = None
user_ids: list[str] = []
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
####################
# Forms
####################
class GroupResponse(BaseModel):
id: str
user_id: str
name: str
description: str
permissions: Optional[dict] = None
data: Optional[dict] = None
meta: Optional[dict] = None
user_ids: list[str] = []
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
class GroupForm(BaseModel):
name: str
description: str
class GroupUpdateForm(GroupForm):
permissions: Optional[dict] = None
user_ids: Optional[list[str]] = None
admin_ids: Optional[list[str]] = None
class GroupTable:
def insert_new_group(
self, user_id: str, form_data: GroupForm
) -> Optional[GroupModel]:
with get_db() as db:
group = GroupModel(
**{
**form_data.model_dump(),
"id": str(uuid.uuid4()),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
result = Group(**group.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return GroupModel.model_validate(result)
else:
return None
except Exception:
return None
def get_groups(self) -> list[GroupModel]:
with get_db() as db:
return [
GroupModel.model_validate(group)
for group in db.query(Group).order_by(Group.updated_at.desc()).all()
]
def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]:
with get_db() as db:
return [
GroupModel.model_validate(group)
for group in db.query(Group)
.filter(
func.json_array_length(Group.user_ids) > 0
) # Ensure array exists
.filter(
Group.user_ids.cast(String).like(f'%"{user_id}"%')
) # String-based check
.order_by(Group.updated_at.desc())
.all()
]
def get_group_by_id(self, id: str) -> Optional[GroupModel]:
try:
with get_db() as db:
group = db.query(Group).filter_by(id=id).first()
return GroupModel.model_validate(group) if group else None
except Exception:
return None
def update_group_by_id(
self, id: str, form_data: GroupUpdateForm, overwrite: bool = False
) -> Optional[GroupModel]:
try:
with get_db() as db:
db.query(Group).filter_by(id=id).update(
{
**form_data.model_dump(exclude_none=True),
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_group_by_id(id=id)
except Exception as e:
log.exception(e)
return None
def delete_group_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Group).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
def delete_all_groups(self) -> bool:
with get_db() as db:
try:
db.query(Group).delete()
db.commit()
return True
except Exception:
return False
Groups = GroupTable()
@@ -0,0 +1,221 @@
import json
import logging
import time
from typing import Optional
import uuid
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from open_webui.apps.webui.models.files import FileMetadataResponse
from open_webui.apps.webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Knowledge DB Schema
####################
class Knowledge(Base):
__tablename__ = "knowledge"
id = Column(Text, unique=True, primary_key=True)
user_id = Column(Text)
name = Column(Text)
description = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class KnowledgeModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
name: str
description: 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 KnowledgeUserModel(KnowledgeModel):
user: Optional[UserResponse] = None
class KnowledgeResponse(KnowledgeModel):
files: Optional[list[FileMetadataResponse | dict]] = None
class KnowledgeUserResponse(KnowledgeUserModel):
files: Optional[list[FileMetadataResponse | dict]] = None
class KnowledgeForm(BaseModel):
name: str
description: str
data: Optional[dict] = None
access_control: Optional[dict] = None
class KnowledgeTable:
def insert_new_knowledge(
self, user_id: str, form_data: KnowledgeForm
) -> Optional[KnowledgeModel]:
with get_db() as db:
knowledge = KnowledgeModel(
**{
**form_data.model_dump(),
"id": str(uuid.uuid4()),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
result = Knowledge(**knowledge.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return KnowledgeModel.model_validate(result)
else:
return None
except Exception:
return None
def get_knowledge_bases(self) -> list[KnowledgeUserModel]:
with get_db() as db:
knowledge_bases = []
for knowledge in (
db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
):
user = Users.get_user_by_id(knowledge.user_id)
knowledge_bases.append(
KnowledgeUserModel.model_validate(
{
**KnowledgeModel.model_validate(knowledge).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return knowledge_bases
def get_knowledge_bases_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[KnowledgeUserModel]:
knowledge_bases = self.get_knowledge_bases()
return [
knowledge_base
for knowledge_base in knowledge_bases
if knowledge_base.user_id == user_id
or has_access(user_id, permission, knowledge_base.access_control)
]
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
try:
with get_db() as db:
knowledge = db.query(Knowledge).filter_by(id=id).first()
return KnowledgeModel.model_validate(knowledge) if knowledge else None
except Exception:
return None
def update_knowledge_by_id(
self, id: str, form_data: KnowledgeForm, overwrite: bool = False
) -> Optional[KnowledgeModel]:
try:
with get_db() as db:
knowledge = self.get_knowledge_by_id(id=id)
db.query(Knowledge).filter_by(id=id).update(
{
**form_data.model_dump(),
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_knowledge_by_id(id=id)
except Exception as e:
log.exception(e)
return None
def update_knowledge_data_by_id(
self, id: str, data: dict
) -> Optional[KnowledgeModel]:
try:
with get_db() as db:
knowledge = self.get_knowledge_by_id(id=id)
db.query(Knowledge).filter_by(id=id).update(
{
"data": data,
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_knowledge_by_id(id=id)
except Exception as e:
log.exception(e)
return None
def delete_knowledge_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Knowledge).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
def delete_all_knowledge(self) -> bool:
with get_db() as db:
try:
db.query(Knowledge).delete()
db.commit()
return True
except Exception:
return False
Knowledges = KnowledgeTable()
+103 -8
View File
@@ -4,8 +4,19 @@ from typing import Optional
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
from open_webui.env import SRC_LOG_LEVELS
from open_webui.apps.webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text
from sqlalchemy import or_, and_, func
from sqlalchemy.dialects import postgresql, sqlite
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
from open_webui.utils.access_control import has_access
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -67,6 +78,25 @@ class Model(Base):
Holds a JSON encoded blob of metadata, see `ModelMeta`.
"""
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
is_active = Column(Boolean, default=True)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
@@ -80,6 +110,9 @@ class ModelModel(BaseModel):
params: ModelParams
meta: ModelMeta
access_control: Optional[dict] = None
is_active: bool
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
@@ -91,12 +124,12 @@ class ModelModel(BaseModel):
####################
class ModelResponse(BaseModel):
id: str
name: str
meta: ModelMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class ModelUserResponse(ModelModel):
user: Optional[UserResponse] = None
class ModelResponse(ModelModel):
pass
class ModelForm(BaseModel):
@@ -105,6 +138,8 @@ class ModelForm(BaseModel):
name: str
meta: ModelMeta
params: ModelParams
access_control: Optional[dict] = None
is_active: bool = True
class ModelsTable:
@@ -138,6 +173,39 @@ class ModelsTable:
with get_db() as db:
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
def get_models(self) -> list[ModelUserResponse]:
with get_db() as db:
models = []
for model in db.query(Model).filter(Model.base_model_id != None).all():
user = Users.get_user_by_id(model.user_id)
models.append(
ModelUserResponse.model_validate(
{
**ModelModel.model_validate(model).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return models
def get_base_models(self) -> list[ModelModel]:
with get_db() as db:
return [
ModelModel.model_validate(model)
for model in db.query(Model).filter(Model.base_model_id == None).all()
]
def get_models_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[ModelUserResponse]:
models = self.get_models()
return [
model
for model in models
if model.user_id == user_id
or has_access(user_id, permission, model.access_control)
]
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
try:
with get_db() as db:
@@ -146,6 +214,23 @@ class ModelsTable:
except Exception:
return None
def toggle_model_by_id(self, id: str) -> Optional[ModelModel]:
with get_db() as db:
try:
is_active = db.query(Model).filter_by(id=id).first().is_active
db.query(Model).filter_by(id=id).update(
{
"is_active": not is_active,
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_model_by_id(id)
except Exception:
return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
try:
with get_db() as db:
@@ -153,7 +238,7 @@ class ModelsTable:
result = (
db.query(Model)
.filter_by(id=id)
.update(model.model_dump(exclude={"id"}, exclude_none=True))
.update(model.model_dump(exclude={"id"}))
)
db.commit()
@@ -175,5 +260,15 @@ class ModelsTable:
except Exception:
return False
def delete_all_models(self) -> bool:
try:
with get_db() as db:
db.query(Model).delete()
db.commit()
return True
except Exception:
return False
Models = ModelsTable()
@@ -2,8 +2,12 @@ import time
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.apps.webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
####################
# Prompts DB Schema
@@ -19,6 +23,23 @@ class Prompt(Base):
content = Column(Text)
timestamp = Column(BigInteger)
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
class PromptModel(BaseModel):
command: str
@@ -27,6 +48,7 @@ class PromptModel(BaseModel):
content: str
timestamp: int # timestamp in epoch
access_control: Optional[dict] = None
model_config = ConfigDict(from_attributes=True)
@@ -35,10 +57,15 @@ class PromptModel(BaseModel):
####################
class PromptUserResponse(PromptModel):
user: Optional[UserResponse] = None
class PromptForm(BaseModel):
command: str
title: str
content: str
access_control: Optional[dict] = None
class PromptsTable:
@@ -48,16 +75,14 @@ class PromptsTable:
prompt = PromptModel(
**{
"user_id": user_id,
"command": form_data.command,
"title": form_data.title,
"content": form_data.content,
**form_data.model_dump(),
"timestamp": int(time.time()),
}
)
try:
with get_db() as db:
result = Prompt(**prompt.dict())
result = Prompt(**prompt.model_dump())
db.add(result)
db.commit()
db.refresh(result)
@@ -76,11 +101,34 @@ class PromptsTable:
except Exception:
return None
def get_prompts(self) -> list[PromptModel]:
def get_prompts(self) -> list[PromptUserResponse]:
with get_db() as db:
return [
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
]
prompts = []
for prompt in db.query(Prompt).order_by(Prompt.timestamp.desc()).all():
user = Users.get_user_by_id(prompt.user_id)
prompts.append(
PromptUserResponse.model_validate(
{
**PromptModel.model_validate(prompt).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return prompts
def get_prompts_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[PromptUserResponse]:
prompts = self.get_prompts()
return [
prompt
for prompt in prompts
if prompt.user_id == user_id
or has_access(user_id, permission, prompt.access_control)
]
def update_prompt_by_command(
self, command: str, form_data: PromptForm
@@ -90,6 +138,7 @@ class PromptsTable:
prompt = db.query(Prompt).filter_by(command=command).first()
prompt.title = form_data.title
prompt.content = form_data.content
prompt.access_control = form_data.access_control
prompt.timestamp = int(time.time())
db.commit()
return PromptModel.model_validate(prompt)
+23 -176
View File
@@ -4,53 +4,35 @@ import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import BigInteger, Column, String, JSON, PrimaryKeyConstraint
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Tag DB Schema
####################
class Tag(Base):
__tablename__ = "tag"
id = Column(String, primary_key=True)
id = Column(String)
name = Column(String)
user_id = Column(String)
data = Column(Text, nullable=True)
meta = Column(JSON, nullable=True)
class ChatIdTag(Base):
__tablename__ = "chatidtag"
id = Column(String, primary_key=True)
tag_name = Column(String)
chat_id = Column(String)
user_id = Column(String)
timestamp = Column(BigInteger)
# Unique constraint ensuring (id, user_id) is unique, not just the `id` column
__table_args__ = (PrimaryKeyConstraint("id", "user_id", name="pk_id_user_id"),)
class TagModel(BaseModel):
id: str
name: str
user_id: str
data: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class ChatIdTagModel(BaseModel):
id: str
tag_name: str
chat_id: str
user_id: str
timestamp: int
meta: Optional[dict] = None
model_config = ConfigDict(from_attributes=True)
@@ -59,23 +41,15 @@ class ChatIdTagModel(BaseModel):
####################
class ChatIdTagForm(BaseModel):
tag_name: str
class TagChatIdForm(BaseModel):
name: str
chat_id: str
class TagChatIdsResponse(BaseModel):
chat_ids: list[str]
class ChatTagsResponse(BaseModel):
tags: list[str]
class TagTable:
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
with get_db() as db:
id = str(uuid.uuid4())
id = name.replace(" ", "_").lower()
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try:
result = Tag(**tag.model_dump())
@@ -86,177 +60,50 @@ class TagTable:
return TagModel.model_validate(result)
else:
return None
except Exception:
except Exception as e:
print(e)
return None
def get_tag_by_name_and_user_id(
self, name: str, user_id: str
) -> Optional[TagModel]:
try:
id = name.replace(" ", "_").lower()
with get_db() as db:
tag = db.query(Tag).filter_by(name=name, user_id=user_id).first()
tag = db.query(Tag).filter_by(id=id, user_id=user_id).first()
return TagModel.model_validate(tag)
except Exception:
return None
def add_tag_to_chat(
self, user_id: str, form_data: ChatIdTagForm
) -> Optional[ChatIdTagModel]:
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
if tag is None:
tag = self.insert_new_tag(form_data.tag_name, user_id)
id = str(uuid.uuid4())
chatIdTag = ChatIdTagModel(
**{
"id": id,
"user_id": user_id,
"chat_id": form_data.chat_id,
"tag_name": tag.name,
"timestamp": int(time.time()),
}
)
try:
with get_db() as db:
result = ChatIdTag(**chatIdTag.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ChatIdTagModel.model_validate(result)
else:
return None
except Exception:
return None
def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
with get_db() as db:
tag_names = [
chat_id_tag.tag_name
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
)
for tag in (db.query(Tag).filter_by(user_id=user_id).all())
]
def get_tags_by_chat_id_and_user_id(
self, chat_id: str, user_id: str
def get_tags_by_ids_and_user_id(
self, ids: list[str], user_id: str
) -> list[TagModel]:
with get_db() as db:
tag_names = [
chat_id_tag.tag_name
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id, chat_id=chat_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).all()
)
]
def get_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> list[ChatIdTagModel]:
with get_db() as db:
return [
ChatIdTagModel.model_validate(chat_id_tag)
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id, tag_name=tag_name)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
def count_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> int:
with get_db() as db:
return (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, user_id=user_id)
.count()
)
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
def delete_tag_by_name_and_user_id(self, name: str, user_id: str) -> bool:
try:
with get_db() as db:
res = (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, user_id=user_id)
.delete()
)
id = name.replace(" ", "_").lower()
res = db.query(Tag).filter_by(id=id, user_id=user_id).delete()
log.debug(f"res: {res}")
db.commit()
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
tag_name, user_id
)
if tag_count == 0:
# Remove tag item from Tag col as well
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
def delete_tag_by_tag_name_and_chat_id_and_user_id(
self, tag_name: str, chat_id: str, user_id: str
) -> bool:
try:
with get_db() as db:
res = (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
.delete()
)
log.debug(f"res: {res}")
db.commit()
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
tag_name, user_id
)
if tag_count == 0:
# Remove tag item from Tag col as well
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
tags = self.get_tags_by_chat_id_and_user_id(chat_id, user_id)
for tag in tags:
self.delete_tag_by_tag_name_and_chat_id_and_user_id(
tag.tag_name, chat_id, user_id
)
return True
Tags = TagTable()
+56 -4
View File
@@ -3,10 +3,13 @@ import time
from typing import Optional
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
from open_webui.apps.webui.models.users import Users
from open_webui.apps.webui.models.users import Users, UserResponse
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -26,6 +29,24 @@ class Tool(Base):
specs = Column(JSONField)
meta = Column(JSONField)
valves = Column(JSONField)
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
@@ -42,6 +63,8 @@ class ToolModel(BaseModel):
content: str
specs: list[dict]
meta: ToolMeta
access_control: Optional[dict] = None
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
@@ -58,15 +81,21 @@ class ToolResponse(BaseModel):
user_id: str
name: str
meta: ToolMeta
access_control: Optional[dict] = None
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class ToolUserResponse(ToolResponse):
user: Optional[UserResponse] = None
class ToolForm(BaseModel):
id: str
name: str
content: str
meta: ToolMeta
access_control: Optional[dict] = None
class ToolValves(BaseModel):
@@ -109,9 +138,32 @@ class ToolsTable:
except Exception:
return None
def get_tools(self) -> list[ToolModel]:
def get_tools(self) -> list[ToolUserResponse]:
with get_db() as db:
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
tools = []
for tool in db.query(Tool).order_by(Tool.updated_at.desc()).all():
user = Users.get_user_by_id(tool.user_id)
tools.append(
ToolUserResponse.model_validate(
{
**ToolModel.model_validate(tool).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return tools
def get_tools_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[ToolUserResponse]:
tools = self.get_tools()
return [
tool
for tool in tools
if tool.user_id == user_id
or has_access(user_id, permission, tool.access_control)
]
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
try:
@@ -62,6 +62,14 @@ class UserModel(BaseModel):
####################
class UserResponse(BaseModel):
id: str
name: str
email: str
role: str
profile_image_url: str
class UserRoleUpdateForm(BaseModel):
id: str
role: str
+358 -9
View File
@@ -1,10 +1,15 @@
import re
import uuid
import time
import datetime
import logging
from open_webui.apps.webui.models.auths import (
AddUserForm,
ApiKey,
Auths,
Token,
LdapForm,
SigninForm,
SigninResponse,
SignupForm,
@@ -13,11 +18,15 @@ from open_webui.apps.webui.models.auths import (
UserResponse,
)
from open_webui.apps.webui.models.users import Users
from open_webui.config import WEBUI_AUTH
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import (
WEBUI_AUTH,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_SESSION_COOKIE_SAME_SITE,
WEBUI_SESSION_COOKIE_SECURE,
SRC_LOG_LEVELS,
)
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import Response
@@ -27,40 +36,78 @@ from open_webui.utils.utils import (
create_api_key,
create_token,
get_admin_user,
get_verified_user,
get_current_user,
get_password_hash,
)
from open_webui.utils.webhook import post_webhook
from open_webui.utils.access_control import get_permissions
from typing import Optional, List
from ssl import CERT_REQUIRED, PROTOCOL_TLS
from ldap3 import Server, Connection, ALL, Tls
from ldap3.utils.conv import escape_filter_chars
router = APIRouter()
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
############################
# GetSessionUser
############################
@router.get("/", response_model=UserResponse)
class SessionUserResponse(Token, UserResponse):
expires_at: Optional[int] = None
permissions: Optional[dict] = None
@router.get("/", response_model=SessionUserResponse)
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)
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,
)
datetime_expires_at = (
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
)
# 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_SESSION_COOKIE_SAME_SITE,
secure=WEBUI_SESSION_COOKIE_SECURE,
)
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
)
return {
"token": token,
"token_type": "Bearer",
"expires_at": expires_at,
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"profile_image_url": user.profile_image_url,
"permissions": user_permissions,
}
@@ -71,7 +118,7 @@ async def get_session_user(
@router.post("/update/profile", response_model=UserResponse)
async def update_profile(
form_data: UpdateProfileForm, session_user=Depends(get_current_user)
form_data: UpdateProfileForm, session_user=Depends(get_verified_user)
):
if session_user:
user = Users.update_user_by_id(
@@ -109,12 +156,152 @@ async def update_password(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
############################
# LDAP Authentication
############################
@router.post("/ldap", response_model=SigninResponse)
async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
ENABLE_LDAP = request.app.state.config.ENABLE_LDAP
LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST
LDAP_SERVER_PORT = request.app.state.config.LDAP_SERVER_PORT
LDAP_ATTRIBUTE_FOR_USERNAME = request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME
LDAP_SEARCH_BASE = request.app.state.config.LDAP_SEARCH_BASE
LDAP_SEARCH_FILTERS = request.app.state.config.LDAP_SEARCH_FILTERS
LDAP_APP_DN = request.app.state.config.LDAP_APP_DN
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
LDAP_CIPHERS = (
request.app.state.config.LDAP_CIPHERS
if request.app.state.config.LDAP_CIPHERS
else "ALL"
)
if not ENABLE_LDAP:
raise HTTPException(400, detail="LDAP authentication is not enabled")
try:
tls = Tls(
validate=CERT_REQUIRED,
version=PROTOCOL_TLS,
ca_certs_file=LDAP_CA_CERT_FILE,
ciphers=LDAP_CIPHERS,
)
except Exception as e:
log.error(f"An error occurred on TLS: {str(e)}")
raise HTTPException(400, detail=str(e))
try:
server = Server(
host=LDAP_SERVER_HOST,
port=LDAP_SERVER_PORT,
get_info=ALL,
use_ssl=LDAP_USE_TLS,
tls=tls,
)
connection_app = Connection(
server,
LDAP_APP_DN,
LDAP_APP_PASSWORD,
auto_bind="NONE",
authentication="SIMPLE",
)
if not connection_app.bind():
raise HTTPException(400, detail="Application account bind failed")
search_success = connection_app.search(
search_base=LDAP_SEARCH_BASE,
search_filter=f"(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})",
attributes=[f"{LDAP_ATTRIBUTE_FOR_USERNAME}", "mail", "cn"],
)
if not search_success:
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["mail"])
cn = str(entry["cn"])
user_dn = entry.entry_dn
if username == form_data.user.lower():
connection_user = Connection(
server,
user_dn,
form_data.password,
auto_bind="NONE",
authentication="SIMPLE",
)
if not connection_user.bind():
raise HTTPException(400, f"Authentication failed for {form_data.user}")
user = Users.get_user_by_email(mail)
if not user:
try:
role = (
"admin"
if Users.get_num_users() == 0
else request.app.state.config.DEFAULT_USER_ROLE
)
user = Auths.insert_new_auth(
email=mail, password=str(uuid.uuid4()), name=cn, role=role
)
if not user:
raise HTTPException(
500, detail=ERROR_MESSAGES.CREATE_USER_ERROR
)
except HTTPException:
raise
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
user = Auths.authenticate_user_by_trusted_header(mail)
if user:
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(
request.app.state.config.JWT_EXPIRES_IN
),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
return {
"token": token,
"token_type": "Bearer",
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"profile_image_url": user.profile_image_url,
}
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}'])}",
)
except Exception as e:
raise HTTPException(400, detail=str(e))
############################
# SignIn
############################
@router.post("/signin", response_model=SigninResponse)
@router.post("/signin", response_model=SessionUserResponse)
async def signin(request: Request, response: Response, form_data: SigninForm):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
@@ -156,26 +343,47 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
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,
)
datetime_expires_at = (
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
)
# 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_SESSION_COOKIE_SAME_SITE,
secure=WEBUI_SESSION_COOKIE_SECURE,
)
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
)
return {
"token": token,
"token_type": "Bearer",
"expires_at": expires_at,
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"profile_image_url": user.profile_image_url,
"permissions": user_permissions,
}
else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
@@ -186,7 +394,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
############################
@router.post("/signup", response_model=SigninResponse)
@router.post("/signup", response_model=SessionUserResponse)
async def signup(request: Request, response: Response, form_data: SignupForm):
if WEBUI_AUTH:
if (
@@ -216,6 +424,11 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
if Users.get_num_users() == 0
else request.app.state.config.DEFAULT_USER_ROLE
)
if Users.get_num_users() == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
@@ -226,16 +439,30 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
)
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,
)
datetime_expires_at = (
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
)
# 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_SESSION_COOKIE_SAME_SITE,
secure=WEBUI_SESSION_COOKIE_SECURE,
)
if request.app.state.config.WEBHOOK_URL:
@@ -249,14 +476,20 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
},
)
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
)
return {
"token": token,
"token_type": "Bearer",
"expires_at": expires_at,
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"profile_image_url": user.profile_image_url,
"permissions": user_permissions,
}
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
@@ -264,6 +497,12 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
@router.get("/signout")
async def signout(response: Response):
response.delete_cookie("token")
return {"status": True}
############################
# AddUser
############################
@@ -348,6 +587,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"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,
@@ -358,6 +598,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
class AdminConfig(BaseModel):
SHOW_ADMIN_DETAILS: bool
ENABLE_SIGNUP: bool
ENABLE_API_KEY: bool
DEFAULT_USER_ROLE: str
JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool
@@ -370,6 +611,7 @@ async def update_admin_config(
):
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
request.app.state.config.ENABLE_API_KEY = form_data.ENABLE_API_KEY
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
@@ -388,6 +630,7 @@ async def update_admin_config(
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"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,
@@ -395,6 +638,105 @@ async def update_admin_config(
}
class LdapServerConfig(BaseModel):
label: str
host: str
port: Optional[int] = None
attribute_for_username: str = "uid"
app_dn: str
app_dn_password: str
search_base: str
search_filters: str = ""
use_tls: bool = True
certificate_path: Optional[str] = None
ciphers: Optional[str] = "ALL"
@router.get("/admin/config/ldap/server", response_model=LdapServerConfig)
async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
return {
"label": request.app.state.config.LDAP_SERVER_LABEL,
"host": request.app.state.config.LDAP_SERVER_HOST,
"port": request.app.state.config.LDAP_SERVER_PORT,
"attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
"app_dn": request.app.state.config.LDAP_APP_DN,
"app_dn_password": request.app.state.config.LDAP_APP_PASSWORD,
"search_base": request.app.state.config.LDAP_SEARCH_BASE,
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"ciphers": request.app.state.config.LDAP_CIPHERS,
}
@router.post("/admin/config/ldap/server")
async def update_ldap_server(
request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)
):
required_fields = [
"label",
"host",
"attribute_for_username",
"app_dn",
"app_dn_password",
"search_base",
]
for key in required_fields:
value = getattr(form_data, key)
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
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = (
form_data.attribute_for_username
)
request.app.state.config.LDAP_APP_DN = form_data.app_dn
request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password
request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters
request.app.state.config.LDAP_USE_TLS = form_data.use_tls
request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path
request.app.state.config.LDAP_CIPHERS = form_data.ciphers
return {
"label": request.app.state.config.LDAP_SERVER_LABEL,
"host": request.app.state.config.LDAP_SERVER_HOST,
"port": request.app.state.config.LDAP_SERVER_PORT,
"attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
"app_dn": request.app.state.config.LDAP_APP_DN,
"app_dn_password": request.app.state.config.LDAP_APP_PASSWORD,
"search_base": request.app.state.config.LDAP_SEARCH_BASE,
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"ciphers": request.app.state.config.LDAP_CIPHERS,
}
@router.get("/admin/config/ldap")
async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
class LdapConfigForm(BaseModel):
enable_ldap: Optional[bool] = None
@router.post("/admin/config/ldap")
async def update_ldap_config(
request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP}
############################
# API Key
############################
@@ -402,9 +744,16 @@ async def update_admin_config(
# create api key
@router.post("/api_key", response_model=ApiKey)
async def create_api_key_(user=Depends(get_current_user)):
async def generate_api_key(request: Request, user=Depends(get_current_user)):
if not request.app.state.config.ENABLE_API_KEY:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.API_KEY_CREATION_NOT_ALLOWED,
)
api_key = create_api_key()
success = Users.update_user_api_key_by_id(user.id, api_key)
if success:
return {
"api_key": api_key,
+281 -93
View File
@@ -4,22 +4,23 @@ from typing import Optional
from open_webui.apps.webui.models.chats import (
ChatForm,
ChatImportForm,
ChatResponse,
Chats,
ChatTitleIdResponse,
)
from open_webui.apps.webui.models.tags import (
ChatIdTagForm,
ChatIdTagModel,
TagModel,
Tags,
)
from open_webui.apps.webui.models.tags import TagModel, Tags
from open_webui.apps.webui.models.folders import Folders
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 fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from open_webui.utils.utils 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"])
@@ -52,9 +53,9 @@ async def get_session_user_chat_list(
@router.delete("/", response_model=bool)
async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
if (
user.role == "user"
and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]
if user.role == "user" and not has_permission(
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -96,7 +97,35 @@ async def get_user_chat_list_by_user_id(
async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
try:
chat = Chats.insert_new_chat(user.id, form_data)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
return ChatResponse(**chat.model_dump())
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# ImportChat
############################
@router.post("/import", response_model=Optional[ChatResponse])
async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user)):
try:
chat = Chats.import_chat(user.id, form_data)
if chat:
tags = chat.meta.get("tags", [])
for tag_id in tags:
tag_id = tag_id.replace(" ", "_").lower()
tag_name = " ".join([word.capitalize() for word in tag_id.split("_")])
if (
tag_id != "none"
and Tags.get_tag_by_name_and_user_id(tag_name, user.id) is None
):
Tags.insert_new_tag(tag_name, user.id)
return ChatResponse(**chat.model_dump())
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -109,10 +138,77 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
############################
@router.get("/search", response_model=list[ChatTitleIdResponse])
async def search_user_chats(
text: str, page: Optional[int] = None, user=Depends(get_verified_user)
):
if page is None:
page = 1
limit = 60
skip = (page - 1) * limit
chat_list = [
ChatTitleIdResponse(**chat.model_dump())
for chat in Chats.get_chats_by_user_id_and_search_text(
user.id, text, skip=skip, limit=limit
)
]
# Delete tag if no chat is found
words = text.strip().split(" ")
if page == 1 and len(words) == 1 and words[0].startswith("tag:"):
tag_id = words[0].replace("tag:", "")
if len(chat_list) == 0:
if Tags.get_tag_by_name_and_user_id(tag_id, user.id):
log.debug(f"deleting tag: {tag_id}")
Tags.delete_tag_by_name_and_user_id(tag_id, user.id)
return chat_list
############################
# GetChatsByFolderId
############################
@router.get("/folder/{folder_id}", response_model=list[ChatResponse])
async def get_chats_by_folder_id(folder_id: str, user=Depends(get_verified_user)):
folder_ids = [folder_id]
children_folders = Folders.get_children_folders_by_id_and_user_id(
folder_id, user.id
)
if children_folders:
folder_ids.extend([folder.id for folder in children_folders])
return [
ChatResponse(**chat.model_dump())
for chat in Chats.get_chats_by_folder_ids_and_user_id(folder_ids, user.id)
]
############################
# GetPinnedChats
############################
@router.get("/pinned", response_model=list[ChatResponse])
async def get_user_pinned_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**chat.model_dump())
for chat in Chats.get_pinned_chats_by_user_id(user.id)
]
############################
# GetChats
############################
@router.get("/all", response_model=list[ChatResponse])
async def get_user_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
ChatResponse(**chat.model_dump())
for chat in Chats.get_chats_by_user_id(user.id)
]
@@ -125,11 +221,28 @@ async def get_user_chats(user=Depends(get_verified_user)):
@router.get("/all/archived", response_model=list[ChatResponse])
async def get_user_archived_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
ChatResponse(**chat.model_dump())
for chat in Chats.get_archived_chats_by_user_id(user.id)
]
############################
# GetAllTags
############################
@router.get("/all/tags", response_model=list[TagModel])
async def get_all_user_tags(user=Depends(get_verified_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetAllChatsInDB
############################
@@ -142,10 +255,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_chats()
]
return [ChatResponse(**chat.model_dump()) for chat in Chats.get_chats()]
############################
@@ -188,7 +298,8 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
return ChatResponse(**chat.model_dump())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -200,48 +311,28 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
############################
class TagNameForm(BaseModel):
class TagForm(BaseModel):
name: str
class TagFilterForm(TagForm):
skip: Optional[int] = 0
limit: Optional[int] = 50
@router.post("/tags", response_model=list[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_verified_user)
form_data: TagFilterForm, user=Depends(get_verified_user)
):
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
form_data.name, user.id
)
]
chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit)
chats = Chats.get_chat_list_by_user_id_and_tag_name(
user.id, form_data.name, form_data.skip, form_data.limit
)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id)
Tags.delete_tag_by_name_and_user_id(form_data.name, user.id)
return chats
############################
# GetAllTags
############################
@router.get("/tags/all", response_model=list[TagModel])
async def get_all_tags(user=Depends(get_verified_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChatById
############################
@@ -252,7 +343,8 @@ async def get_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
return ChatResponse(**chat.model_dump())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -270,10 +362,9 @@ async def update_chat_by_id(
):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
updated_chat = {**json.loads(chat.chat), **form_data.chat}
updated_chat = {**chat.chat, **form_data.chat}
chat = Chats.update_chat_by_id(id, updated_chat)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
return ChatResponse(**chat.model_dump())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -289,38 +380,83 @@ async def update_chat_by_id(
@router.delete("/{id}", response_model=bool)
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role == "admin":
chat = Chats.get_chat_by_id(id)
for tag in chat.meta.get("tags", []):
if Chats.count_chats_by_tag_name_and_user_id(tag, user.id) == 1:
Tags.delete_tag_by_name_and_user_id(tag, user.id)
result = Chats.delete_chat_by_id(id)
return result
else:
if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]:
if not has_permission(
user.id, "chat.delete", 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(id)
for tag in chat.meta.get("tags", []):
if Chats.count_chats_by_tag_name_and_user_id(tag, user.id) == 1:
Tags.delete_tag_by_name_and_user_id(tag, user.id)
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
return result
############################
# GetPinnedStatusById
############################
@router.get("/{id}/pinned", response_model=Optional[bool])
async def get_pinned_status_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
return chat.pinned
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# PinChatById
############################
@router.post("/{id}/pin", response_model=Optional[ChatResponse])
async def pin_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat = Chats.toggle_chat_pinned_by_id(id)
return chat
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# CloneChat
############################
@router.get("/{id}/clone", response_model=Optional[ChatResponse])
@router.post("/{id}/clone", response_model=Optional[ChatResponse])
async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat_body = json.loads(chat.chat)
updated_chat = {
**chat_body,
**chat.chat,
"originalChatId": chat.id,
"branchPointMessageId": chat_body["history"]["currentId"],
"branchPointMessageId": chat.chat["history"]["currentId"],
"title": f"Clone of {chat.title}",
}
chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
return ChatResponse(**chat.model_dump())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
@@ -332,12 +468,26 @@ async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
############################
@router.get("/{id}/archive", response_model=Optional[ChatResponse])
@router.post("/{id}/archive", response_model=Optional[ChatResponse])
async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat = Chats.toggle_chat_archive_by_id(id)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
# Delete tags if chat is archived
if chat.archived:
for tag_id in chat.meta.get("tags", []):
if Chats.count_chats_by_tag_name_and_user_id(tag_id, user.id) == 0:
log.debug(f"deleting tag: {tag_id}")
Tags.delete_tag_by_name_and_user_id(tag_id, user.id)
else:
for tag_id in chat.meta.get("tags", []):
tag = Tags.get_tag_by_name_and_user_id(tag_id, user.id)
if tag is None:
log.debug(f"inserting tag: {tag_id}")
tag = Tags.insert_new_tag(tag_id, user.id)
return ChatResponse(**chat.model_dump())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
@@ -355,9 +505,7 @@ async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
return ChatResponse(**shared_chat.model_dump())
shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
if not shared_chat:
@@ -365,10 +513,8 @@ async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(),
)
return ChatResponse(**shared_chat.model_dump())
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -399,6 +545,31 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
)
############################
# UpdateChatFolderIdById
############################
class ChatFolderIdForm(BaseModel):
folder_id: Optional[str] = None
@router.post("/{id}/folder", response_model=Optional[ChatResponse])
async def update_chat_folder_id_by_id(
id: str, form_data: ChatFolderIdForm, user=Depends(get_verified_user)
):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat = Chats.update_chat_folder_id_by_id_and_user_id(
id, user.id, form_data.folder_id
)
return ChatResponse(**chat.model_dump())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChatTagsById
############################
@@ -406,10 +577,10 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
@router.get("/{id}/tags", response_model=list[TagModel])
async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
if tags != None:
return tags
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
tags = chat.meta.get("tags", [])
return Tags.get_tags_by_ids_and_user_id(tags, user.id)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -421,22 +592,30 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
############################
@router.post("/{id}/tags", response_model=Optional[ChatIdTagModel])
async def add_chat_tag_by_id(
id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
@router.post("/{id}/tags", response_model=list[TagModel])
async def add_tag_by_id_and_tag_name(
id: str, form_data: TagForm, user=Depends(get_verified_user)
):
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
tags = chat.meta.get("tags", [])
tag_id = form_data.name.replace(" ", "_").lower()
if form_data.tag_name not in tags:
tag = Tags.add_tag_to_chat(user.id, form_data)
if tag:
return tag
else:
if tag_id == "none":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Tag name cannot be 'None'"),
)
print(tags, tag_id)
if tag_id not in tags:
Chats.add_chat_tag_by_id_and_user_id_and_tag_name(
id, user.id, form_data.name
)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
tags = chat.meta.get("tags", [])
return Tags.get_tags_by_ids_and_user_id(tags, user.id)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
@@ -448,16 +627,20 @@ async def add_chat_tag_by_id(
############################
@router.delete("/{id}/tags", response_model=Optional[bool])
async def delete_chat_tag_by_id(
id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
@router.delete("/{id}/tags", response_model=list[TagModel])
async def delete_tag_by_id_and_tag_name(
id: str, form_data: TagForm, user=Depends(get_verified_user)
):
result = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id(
form_data.tag_name, id, user.id
)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
Chats.delete_tag_by_id_and_user_id_and_tag_name(id, user.id, form_data.name)
if result:
return result
if Chats.count_chats_by_tag_name_and_user_id(form_data.name, user.id) == 0:
Tags.delete_tag_by_name_and_user_id(form_data.name, user.id)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
tags = chat.meta.get("tags", [])
return Tags.get_tags_by_ids_and_user_id(tags, user.id)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -465,16 +648,21 @@ async def delete_chat_tag_by_id(
############################
# DeleteAllChatTagsById
# DeleteAllTagsById
############################
@router.delete("/{id}/tags/all", response_model=Optional[bool])
async def delete_all_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
result = Tags.delete_tags_by_chat_id_and_user_id(id, user.id)
async def delete_all_tags_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
Chats.delete_all_tags_by_id_and_user_id(id, user.id)
if result:
return result
for tag in chat.meta.get("tags", []):
if Chats.count_chats_by_tag_name_and_user_id(tag, user.id) == 0:
Tags.delete_tag_by_name_and_user_id(tag, user.id)
return True
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -1,10 +1,12 @@
from open_webui.config import BannerModel
from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from typing import Optional
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.config import get_config, save_config
from open_webui.config import BannerModel
router = APIRouter()
@@ -34,8 +36,32 @@ async def export_config(user=Depends(get_admin_user)):
return get_config()
class SetDefaultModelsForm(BaseModel):
models: str
############################
# SetDefaultModels
############################
class ModelsConfigForm(BaseModel):
DEFAULT_MODELS: Optional[str]
MODEL_ORDER_LIST: Optional[list[str]]
@router.get("/models", response_model=ModelsConfigForm)
async def get_models_config(request: Request, user=Depends(get_admin_user)):
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
}
@router.post("/models", response_model=ModelsConfigForm)
async def set_models_config(
request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS
request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
}
class PromptSuggestion(BaseModel):
@@ -47,21 +73,8 @@ class SetDefaultSuggestionsForm(BaseModel):
suggestions: list[PromptSuggestion]
############################
# SetDefaultModels
############################
@router.post("/default/models", response_model=str)
async def set_global_default_models(
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user)
):
request.app.state.config.DEFAULT_MODELS = form_data.models
return request.app.state.config.DEFAULT_MODELS
@router.post("/default/suggestions", response_model=list[PromptSuggestion])
async def set_global_default_suggestions(
@router.post("/suggestions", response_model=list[PromptSuggestion])
async def set_default_suggestions(
request: Request,
form_data: SetDefaultSuggestionsForm,
user=Depends(get_admin_user),
@@ -1,155 +0,0 @@
import json
from typing import Optional
from open_webui.apps.webui.models.documents import (
DocumentForm,
DocumentResponse,
Documents,
DocumentUpdateForm,
)
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from open_webui.utils.utils import get_admin_user, get_verified_user
router = APIRouter()
############################
# GetDocuments
############################
@router.get("/", response_model=list[DocumentResponse])
async def get_documents(user=Depends(get_verified_user)):
docs = [
DocumentResponse(
**{
**doc.model_dump(),
"content": json.loads(doc.content if doc.content else "{}"),
}
)
for doc in Documents.get_docs()
]
return docs
############################
# CreateNewDoc
############################
@router.post("/create", response_model=Optional[DocumentResponse])
async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
doc = Documents.get_doc_by_name(form_data.name)
if doc is None:
doc = Documents.insert_new_doc(user.id, form_data)
if doc:
return DocumentResponse(
**{
**doc.model_dump(),
"content": json.loads(doc.content if doc.content else "{}"),
}
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_EXISTS,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NAME_TAG_TAKEN,
)
############################
# GetDocByName
############################
@router.get("/doc", response_model=Optional[DocumentResponse])
async def get_doc_by_name(name: str, user=Depends(get_verified_user)):
doc = Documents.get_doc_by_name(name)
if doc:
return DocumentResponse(
**{
**doc.model_dump(),
"content": json.loads(doc.content if doc.content else "{}"),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# TagDocByName
############################
class TagItem(BaseModel):
name: str
class TagDocumentForm(BaseModel):
name: str
tags: list[dict]
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_user)):
doc = Documents.update_doc_content_by_name(form_data.name, {"tags": form_data.tags})
if doc:
return DocumentResponse(
**{
**doc.model_dump(),
"content": json.loads(doc.content if doc.content else "{}"),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateDocByName
############################
@router.post("/doc/update", response_model=Optional[DocumentResponse])
async def update_doc_by_name(
name: str,
form_data: DocumentUpdateForm,
user=Depends(get_admin_user),
):
doc = Documents.update_doc_by_name(name, form_data)
if doc:
return DocumentResponse(
**{
**doc.model_dump(),
"content": json.loads(doc.content if doc.content else "{}"),
}
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NAME_TAG_TAKEN,
)
############################
# DeleteDocByName
############################
@router.delete("/doc/delete", response_model=bool)
async def delete_doc_by_name(name: str, user=Depends(get_admin_user)):
result = Documents.delete_doc_by_name(name)
return result
@@ -0,0 +1,159 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from pydantic import BaseModel
from open_webui.apps.webui.models.users import Users, UserModel
from open_webui.apps.webui.models.feedbacks import (
FeedbackModel,
FeedbackResponse,
FeedbackForm,
Feedbacks,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.utils.utils import get_admin_user, get_verified_user
router = APIRouter()
############################
# GetConfig
############################
@router.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_EVALUATION_ARENA_MODELS": request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS,
"EVALUATION_ARENA_MODELS": request.app.state.config.EVALUATION_ARENA_MODELS,
}
############################
# UpdateConfig
############################
class UpdateConfigForm(BaseModel):
ENABLE_EVALUATION_ARENA_MODELS: Optional[bool] = None
EVALUATION_ARENA_MODELS: Optional[list[dict]] = None
@router.post("/config")
async def update_config(
request: Request,
form_data: UpdateConfigForm,
user=Depends(get_admin_user),
):
config = request.app.state.config
if form_data.ENABLE_EVALUATION_ARENA_MODELS is not None:
config.ENABLE_EVALUATION_ARENA_MODELS = form_data.ENABLE_EVALUATION_ARENA_MODELS
if form_data.EVALUATION_ARENA_MODELS is not None:
config.EVALUATION_ARENA_MODELS = form_data.EVALUATION_ARENA_MODELS
return {
"ENABLE_EVALUATION_ARENA_MODELS": config.ENABLE_EVALUATION_ARENA_MODELS,
"EVALUATION_ARENA_MODELS": config.EVALUATION_ARENA_MODELS,
}
class FeedbackUserResponse(FeedbackResponse):
user: Optional[UserModel] = None
@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
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)
)
for feedback in feedbacks
]
@router.delete("/feedbacks/all")
async def delete_all_feedbacks(user=Depends(get_admin_user)):
success = Feedbacks.delete_all_feedbacks()
return success
@router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks()
return [
FeedbackModel(
**feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
)
for feedback in feedbacks
]
@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
async def get_feedbacks(user=Depends(get_verified_user)):
feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id)
return feedbacks
@router.delete("/feedbacks", response_model=bool)
async def delete_feedbacks(user=Depends(get_verified_user)):
success = Feedbacks.delete_feedbacks_by_user_id(user.id)
return success
@router.post("/feedback", response_model=FeedbackModel)
async def create_feedback(
request: Request,
form_data: FeedbackForm,
user=Depends(get_verified_user),
):
feedback = Feedbacks.insert_new_feedback(user_id=user.id, form_data=form_data)
if not feedback:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(),
)
return feedback
@router.get("/feedback/{id}", response_model=FeedbackModel)
async def get_feedback_by_id(id: str, user=Depends(get_verified_user)):
feedback = Feedbacks.get_feedback_by_id_and_user_id(id=id, user_id=user.id)
if not feedback:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
return feedback
@router.post("/feedback/{id}", response_model=FeedbackModel)
async def update_feedback_by_id(
id: str, form_data: FeedbackForm, user=Depends(get_verified_user)
):
feedback = Feedbacks.update_feedback_by_id_and_user_id(
id=id, user_id=user.id, form_data=form_data
)
if not feedback:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
return feedback
@router.delete("/feedback/{id}")
async def delete_feedback_by_id(id: str, user=Depends(get_verified_user)):
if user.role == "admin":
success = Feedbacks.delete_feedback_by_id(id=id)
else:
success = Feedbacks.delete_feedback_by_id_and_user_id(id=id, user_id=user.id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
return success
+185 -54
View File
@@ -1,16 +1,30 @@
import logging
import os
import shutil
import uuid
from pathlib import Path
from typing import Optional
from pydantic import BaseModel
import mimetypes
from open_webui.storage.provider import Storage
from open_webui.apps.webui.models.files import (
FileForm,
FileModel,
FileModelResponse,
Files,
)
from open_webui.apps.retrieval.main import process_file, ProcessFileForm
from open_webui.apps.webui.models.files import FileForm, FileModel, Files
from open_webui.config import UPLOAD_DIR
from open_webui.constants import ERROR_MESSAGES
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.responses import FileResponse
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.utils.utils import get_admin_user, get_verified_user
log = logging.getLogger(__name__)
@@ -24,7 +38,7 @@ router = APIRouter()
############################
@router.post("/")
@router.post("/", response_model=FileModelResponse)
def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
log.info(f"file.content_type: {file.content_type}")
try:
@@ -35,31 +49,39 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
id = str(uuid.uuid4())
name = filename
filename = f"{id}_{filename}"
file_path = f"{UPLOAD_DIR}/{filename}"
contents, file_path = Storage.upload_file(file.file, filename)
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
f.close()
file = Files.insert_new_file(
file_item = Files.insert_new_file(
user.id,
FileForm(
**{
"id": id,
"filename": filename,
"filename": name,
"path": file_path,
"meta": {
"name": name,
"content_type": file.content_type,
"size": len(contents),
"path": file_path,
},
}
),
)
if file:
return file
try:
process_file(ProcessFileForm(file_id=id))
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
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -79,7 +101,7 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
############################
@router.get("/", response_model=list[FileModel])
@router.get("/", response_model=list[FileModelResponse])
async def list_files(user=Depends(get_verified_user)):
if user.role == "admin":
files = Files.get_files()
@@ -96,27 +118,16 @@ async def list_files(user=Depends(get_verified_user)):
@router.delete("/all")
async def delete_all_files(user=Depends(get_admin_user)):
result = Files.delete_all_files()
if result:
folder = f"{UPLOAD_DIR}"
try:
# Check if the directory exists
if os.path.exists(folder):
# Iterate over all the files and directories in the specified directory
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path) # Remove the file or link
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"The directory {folder} does not exist")
Storage.delete_all_files()
except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}")
log.exception(e)
log.error(f"Error deleting files")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
)
return {"message": "All files deleted successfully"}
else:
raise HTTPException(
@@ -143,26 +154,86 @@ async def get_file_by_id(id: str, user=Depends(get_verified_user)):
)
############################
# Get File Data Content By Id
############################
@router.get("/{id}/data/content")
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"):
return {"content": file.data.get("content", "")}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Update File Data Content By Id
############################
class ContentForm(BaseModel):
content: str
@router.post("/{id}/data/content/update")
async def update_file_data_content_by_id(
id: str, form_data: ContentForm, user=Depends(get_verified_user)
):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
try:
process_file(ProcessFileForm(file_id=id, content=form_data.content))
file = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
log.error(f"Error processing file: {file.id}")
return {"content": file.data.get("content", "")}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Get File Content By Id
############################
@router.get("/{id}/content", response_model=Optional[FileModel])
@router.get("/{id}/content")
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"):
file_path = Path(file.meta["path"])
try:
file_path = Storage.get_file(file.path)
file_path = Path(file_path)
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
else:
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
headers = {
"Content-Disposition": f'attachment; filename="{file.meta.get("name", file.filename)}"'
}
return FileResponse(file_path, headers=headers)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
except Exception as e:
log.exception(e)
log.error(f"Error getting file content")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error getting file content"),
)
else:
raise HTTPException(
@@ -171,21 +242,72 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
)
@router.get("/{id}/content/{file_name}", response_model=Optional[FileModel])
@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"):
try:
file_path = Storage.get_file(file.path)
file_path = Path(file_path)
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
except Exception as e:
log.exception(e)
log.error(f"Error getting file content")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error getting file content"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/{id}/content/{file_name}")
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"):
file_path = Path(file.meta["path"])
file_path = file.path
if file_path:
file_path = Storage.get_file(file_path)
file_path = Path(file_path)
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
headers = {
"Content-Disposition": f'attachment; filename="{file.meta.get("name", file.filename)}"'
}
return FileResponse(file_path, headers=headers)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
# File path doesnt exist, return the content as .txt if possible
file_content = file.content.get("content", "")
file_name = file.filename
# Create a generator that encodes the file content
def generator():
yield file_content.encode("utf-8")
return StreamingResponse(
generator(),
media_type="text/plain",
headers={"Content-Disposition": f"attachment; filename={file_name}"},
)
else:
raise HTTPException(
@@ -205,6 +327,15 @@ async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
if file and (file.user_id == user.id or user.role == "admin"):
result = Files.delete_file_by_id(id)
if result:
try:
Storage.delete_file(file.filename)
except Exception as e:
log.exception(e)
log.error(f"Error deleting files")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
)
return {"message": "File deleted successfully"}
else:
raise HTTPException(
@@ -0,0 +1,251 @@
import logging
import os
import shutil
import uuid
from pathlib import Path
from typing import Optional
from pydantic import BaseModel
import mimetypes
from open_webui.apps.webui.models.folders import (
FolderForm,
FolderModel,
Folders,
)
from open_webui.apps.webui.models.chats import Chats
from open_webui.config import UPLOAD_DIR
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.responses import FileResponse, StreamingResponse
from open_webui.utils.utils import get_admin_user, get_verified_user
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# Get Folders
############################
@router.get("/", response_model=list[FolderModel])
async def get_folders(user=Depends(get_verified_user)):
folders = Folders.get_folders_by_user_id(user.id)
return [
{
**folder.model_dump(),
"items": {
"chats": [
{"title": chat.title, "id": chat.id}
for chat in Chats.get_chats_by_folder_id_and_user_id(
folder.id, user.id
)
]
},
}
for folder in folders
]
############################
# Create Folder
############################
@router.post("/")
def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
None, user.id, form_data.name
)
if folder:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
)
try:
folder = Folders.insert_new_folder(user.id, form_data.name)
return folder
except Exception as e:
log.exception(e)
log.error("Error creating folder")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating folder"),
)
############################
# Get Folders By Id
############################
@router.get("/{id}", response_model=Optional[FolderModel])
async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
return folder
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Update Folder Name By Id
############################
@router.post("/{id}/update")
async def update_folder_name_by_id(
id: str, form_data: FolderForm, user=Depends(get_verified_user)
):
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
folder.parent_id, user.id, form_data.name
)
if existing_folder:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
)
try:
folder = Folders.update_folder_name_by_id_and_user_id(
id, user.id, form_data.name
)
return folder
except Exception as e:
log.exception(e)
log.error(f"Error updating folder: {id}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Update Folder Parent Id By Id
############################
class FolderParentIdForm(BaseModel):
parent_id: Optional[str] = None
@router.post("/{id}/update/parent")
async def update_folder_parent_id_by_id(
id: str, form_data: FolderParentIdForm, user=Depends(get_verified_user)
):
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
form_data.parent_id, user.id, folder.name
)
if existing_folder:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
)
try:
folder = Folders.update_folder_parent_id_by_id_and_user_id(
id, user.id, form_data.parent_id
)
return folder
except Exception as e:
log.exception(e)
log.error(f"Error updating folder: {id}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Update Folder Is Expanded By Id
############################
class FolderIsExpandedForm(BaseModel):
is_expanded: bool
@router.post("/{id}/update/expanded")
async def update_folder_is_expanded_by_id(
id: str, form_data: FolderIsExpandedForm, user=Depends(get_verified_user)
):
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
try:
folder = Folders.update_folder_is_expanded_by_id_and_user_id(
id, user.id, form_data.is_expanded
)
return folder
except Exception as e:
log.exception(e)
log.error(f"Error updating folder: {id}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Delete Folder By Id
############################
@router.delete("/{id}")
async def delete_folder_by_id(id: str, user=Depends(get_verified_user)):
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
try:
result = Folders.delete_folder_by_id_and_user_id(id, user.id)
if result:
return result
else:
raise Exception("Error deleting folder")
except Exception as e:
log.exception(e)
log.error(f"Error deleting folder: {id}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@@ -9,7 +9,7 @@ from open_webui.apps.webui.models.functions import (
Functions,
)
from open_webui.apps.webui.utils import load_function_module_by_id, replace_imports
from open_webui.config import CACHE_DIR, FUNCTIONS_DIR
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.utils import get_admin_user, get_verified_user
@@ -0,0 +1,120 @@
import os
from pathlib import Path
from typing import Optional
from open_webui.apps.webui.models.groups import (
Groups,
GroupForm,
GroupUpdateForm,
GroupResponse,
)
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.utils import get_admin_user, get_verified_user
router = APIRouter()
############################
# GetFunctions
############################
@router.get("/", response_model=list[GroupResponse])
async def get_groups(user=Depends(get_verified_user)):
if user.role == "admin":
return Groups.get_groups()
else:
return Groups.get_groups_by_member_id(user.id)
############################
# CreateNewGroup
############################
@router.post("/create", response_model=Optional[GroupResponse])
async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)):
try:
group = Groups.insert_new_group(user.id, form_data)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# GetGroupById
############################
@router.get("/id/{id}", response_model=Optional[GroupResponse])
async def get_group_by_id(id: str, user=Depends(get_admin_user)):
group = Groups.get_group_by_id(id)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateGroupById
############################
@router.post("/id/{id}/update", response_model=Optional[GroupResponse])
async def update_group_by_id(
id: str, form_data: GroupUpdateForm, user=Depends(get_admin_user)
):
try:
group = Groups.update_group_by_id(id, form_data)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# DeleteGroupById
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_group_by_id(id: str, user=Depends(get_admin_user)):
try:
result = Groups.delete_group_by_id(id)
if result:
return result
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@@ -0,0 +1,510 @@
import json
from typing import Optional, Union
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, status, Request
import logging
from open_webui.apps.webui.models.knowledge import (
Knowledges,
KnowledgeForm,
KnowledgeResponse,
KnowledgeUserResponse,
)
from open_webui.apps.webui.models.files import Files, FileModel
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.apps.retrieval.main import process_file, ProcessFileForm
from open_webui.constants import ERROR_MESSAGES
from open_webui.utils.utils 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
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# getKnowledgeBases
############################
@router.get("/", response_model=list[KnowledgeUserResponse])
async def get_knowledge(user=Depends(get_verified_user)):
knowledge_bases = []
if user.role == "admin":
knowledge_bases = Knowledges.get_knowledge_bases()
else:
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
# Get files for each knowledge base
knowledge_with_files = []
for knowledge_base in knowledge_bases:
files = []
if knowledge_base.data:
files = Files.get_file_metadatas_by_ids(
knowledge_base.data.get("file_ids", [])
)
# Check if all files exist
if len(files) != len(knowledge_base.data.get("file_ids", [])):
missing_files = list(
set(knowledge_base.data.get("file_ids", []))
- set([file.id for file in files])
)
if missing_files:
data = knowledge_base.data or {}
file_ids = data.get("file_ids", [])
for missing_file in missing_files:
file_ids.remove(missing_file)
data["file_ids"] = file_ids
Knowledges.update_knowledge_data_by_id(
id=knowledge_base.id, data=data
)
files = Files.get_file_metadatas_by_ids(file_ids)
knowledge_with_files.append(
KnowledgeUserResponse(
**knowledge_base.model_dump(),
files=files,
)
)
return knowledge_with_files
@router.get("/list", response_model=list[KnowledgeUserResponse])
async def get_knowledge_list(user=Depends(get_verified_user)):
knowledge_bases = []
if user.role == "admin":
knowledge_bases = Knowledges.get_knowledge_bases()
else:
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "write")
# Get files for each knowledge base
knowledge_with_files = []
for knowledge_base in knowledge_bases:
files = []
if knowledge_base.data:
files = Files.get_file_metadatas_by_ids(
knowledge_base.data.get("file_ids", [])
)
# Check if all files exist
if len(files) != len(knowledge_base.data.get("file_ids", [])):
missing_files = list(
set(knowledge_base.data.get("file_ids", []))
- set([file.id for file in files])
)
if missing_files:
data = knowledge_base.data or {}
file_ids = data.get("file_ids", [])
for missing_file in missing_files:
file_ids.remove(missing_file)
data["file_ids"] = file_ids
Knowledges.update_knowledge_data_by_id(
id=knowledge_base.id, data=data
)
files = Files.get_file_metadatas_by_ids(file_ids)
knowledge_with_files.append(
KnowledgeUserResponse(
**knowledge_base.model_dump(),
files=files,
)
)
return knowledge_with_files
############################
# CreateNewKnowledge
############################
@router.post("/create", response_model=Optional[KnowledgeResponse])
async def create_new_knowledge(
request: Request, form_data: KnowledgeForm, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
if knowledge:
return knowledge
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_EXISTS,
)
############################
# GetKnowledgeById
############################
class KnowledgeFilesResponse(KnowledgeResponse):
files: list[FileModel]
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if knowledge:
if (
user.role == "admin"
or knowledge.user_id == user.id
or has_access(user.id, "read", knowledge.access_control)
):
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
files = Files.get_files_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=files,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateKnowledgeById
############################
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
async def update_knowledge_by_id(
id: str,
form_data: KnowledgeForm,
user=Depends(get_verified_user),
):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if knowledge.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
if knowledge:
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
files = Files.get_files_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=files,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ID_TAKEN,
)
############################
# AddFileToKnowledge
############################
class KnowledgeFileIdForm(BaseModel):
file_id: str
@router.post("/{id}/file/add", response_model=Optional[KnowledgeFilesResponse])
def add_file_to_knowledge_by_id(
id: str,
form_data: KnowledgeFileIdForm,
user=Depends(get_verified_user),
):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if knowledge.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
file = Files.get_file_by_id(form_data.file_id)
if not file:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not file.data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_PROCESSED,
)
# Add content to the vector database
try:
process_file(ProcessFileForm(file_id=form_data.file_id, collection_name=id))
except Exception as e:
log.debug(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
if knowledge:
data = knowledge.data or {}
file_ids = data.get("file_ids", [])
if form_data.file_id not in file_ids:
file_ids.append(form_data.file_id)
data["file_ids"] = file_ids
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=files,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("knowledge"),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("file_id"),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.post("/{id}/file/update", response_model=Optional[KnowledgeFilesResponse])
def update_file_from_knowledge_by_id(
id: str,
form_data: KnowledgeFileIdForm,
user=Depends(get_verified_user),
):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if knowledge.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
file = Files.get_file_by_id(form_data.file_id)
if not file:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# Remove content from the vector database
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
)
# Add content to the vector database
try:
process_file(ProcessFileForm(file_id=form_data.file_id, collection_name=id))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
if knowledge:
data = knowledge.data or {}
file_ids = data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=files,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# RemoveFileFromKnowledge
############################
@router.post("/{id}/file/remove", response_model=Optional[KnowledgeFilesResponse])
def remove_file_from_knowledge_by_id(
id: str,
form_data: KnowledgeFileIdForm,
user=Depends(get_verified_user),
):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if knowledge.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
file = Files.get_file_by_id(form_data.file_id)
if not file:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# Remove content from the vector database
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
)
result = VECTOR_DB_CLIENT.query(
collection_name=knowledge.id,
filter={"file_id": form_data.file_id},
)
Files.delete_file_by_id(form_data.file_id)
if knowledge:
data = knowledge.data or {}
file_ids = data.get("file_ids", [])
if form_data.file_id in file_ids:
file_ids.remove(form_data.file_id)
data["file_ids"] = file_ids
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=files,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("knowledge"),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("file_id"),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# DeleteKnowledgeById
############################
@router.delete("/{id}/delete", response_model=bool)
async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if knowledge.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
try:
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
except Exception as e:
log.debug(e)
pass
result = Knowledges.delete_knowledge_by_id(id=id)
return result
############################
# ResetKnowledgeById
############################
@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
knowledge = Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if knowledge.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
try:
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
except Exception as e:
log.debug(e)
pass
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data={"file_ids": []})
return knowledge
@@ -4,7 +4,7 @@ import logging
from typing import Optional
from open_webui.apps.webui.models.memories import Memories, MemoryModel
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.utils import get_verified_user
from open_webui.env import SRC_LOG_LEVELS
+126 -41
View File
@@ -4,53 +4,71 @@ from open_webui.apps.webui.models.models import (
ModelForm,
ModelModel,
ModelResponse,
ModelUserResponse,
Models,
)
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
router = APIRouter()
###########################
# getModels
# GetModels
###########################
@router.get("/", response_model=list[ModelResponse])
@router.get("/", response_model=list[ModelUserResponse])
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
if id:
model = Models.get_model_by_id(id)
if model:
return [model]
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role == "admin":
return Models.get_models()
else:
return Models.get_all_models()
return Models.get_models_by_user_id(user.id)
###########################
# GetBaseModels
###########################
@router.get("/base", response_model=list[ModelResponse])
async def get_base_models(user=Depends(get_admin_user)):
return Models.get_base_models()
############################
# AddNewModel
# CreateNewModel
############################
@router.post("/add", response_model=Optional[ModelModel])
async def add_new_model(
@router.post("/create", response_model=Optional[ModelModel])
async def create_new_model(
request: Request,
form_data: ModelForm,
user=Depends(get_admin_user),
user=Depends(get_verified_user),
):
if form_data.id in request.app.state.MODELS:
if user.role != "admin" and not has_permission(
user.id, "workspace.models", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
model = Models.get_model_by_id(form_data.id)
if model:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
)
else:
model = Models.insert_new_model(form_data, user.id)
if model:
return model
else:
@@ -60,37 +78,85 @@ async def add_new_model(
)
###########################
# GetModelById
###########################
# Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id
@router.get("/model", response_model=Optional[ModelResponse])
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
if model:
if (
user.role == "admin"
or model.user_id == user.id
or has_access(user.id, "read", model.access_control)
):
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToggelModelById
############################
@router.post("/model/toggle", response_model=Optional[ModelResponse])
async def toggle_model_by_id(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
if model:
if (
user.role == "admin"
or model.user_id == user.id
or has_access(user.id, "write", model.access_control)
):
model = Models.toggle_model_by_id(id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateModelById
############################
@router.post("/update", response_model=Optional[ModelModel])
@router.post("/model/update", response_model=Optional[ModelModel])
async def update_model_by_id(
request: Request,
id: str,
form_data: ModelForm,
user=Depends(get_admin_user),
user=Depends(get_verified_user),
):
model = Models.get_model_by_id(id)
if model:
model = Models.update_model_by_id(id, form_data)
return model
else:
if form_data.id in request.app.state.MODELS:
model = Models.insert_new_model(form_data, user.id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
if not model:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
model = Models.update_model_by_id(id, form_data)
return model
############################
@@ -98,7 +164,26 @@ async def update_model_by_id(
############################
@router.delete("/delete", response_model=bool)
async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
@router.delete("/model/delete", response_model=bool)
async def delete_model_by_id(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
if not model:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if model.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
result = Models.delete_model_by_id(id)
return result
@router.delete("/delete/all", response_model=bool)
async def delete_all_models(user=Depends(get_admin_user)):
result = Models.delete_all_models()
return result
@@ -1,9 +1,15 @@
from typing import Optional
from open_webui.apps.webui.models.prompts import PromptForm, PromptModel, Prompts
from open_webui.apps.webui.models.prompts import (
PromptForm,
PromptUserResponse,
PromptModel,
Prompts,
)
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
router = APIRouter()
@@ -14,7 +20,22 @@ router = APIRouter()
@router.get("/", response_model=list[PromptModel])
async def get_prompts(user=Depends(get_verified_user)):
return Prompts.get_prompts()
if user.role == "admin":
prompts = Prompts.get_prompts()
else:
prompts = Prompts.get_prompts_by_user_id(user.id, "read")
return prompts
@router.get("/list", response_model=list[PromptUserResponse])
async def get_prompt_list(user=Depends(get_verified_user)):
if user.role == "admin":
prompts = Prompts.get_prompts()
else:
prompts = Prompts.get_prompts_by_user_id(user.id, "write")
return prompts
############################
@@ -23,7 +44,17 @@ async def get_prompts(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[PromptModel])
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
async def create_new_prompt(
request: Request, form_data: PromptForm, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "workspace.prompts", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
prompt = Prompts.get_prompt_by_command(form_data.command)
if prompt is None:
prompt = Prompts.insert_new_prompt(user.id, form_data)
@@ -50,7 +81,12 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
prompt = Prompts.get_prompt_by_command(f"/{command}")
if prompt:
return prompt
if (
user.role == "admin"
or prompt.user_id == user.id
or has_access(user.id, "read", prompt.access_control)
):
return prompt
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -67,8 +103,21 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
async def update_prompt_by_command(
command: str,
form_data: PromptForm,
user=Depends(get_admin_user),
user=Depends(get_verified_user),
):
prompt = Prompts.get_prompt_by_command(f"/{command}")
if not prompt:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if prompt.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
if prompt:
return prompt
@@ -85,6 +134,19 @@ async def update_prompt_by_command(
@router.delete("/command/{command}/delete", response_model=bool)
async def delete_prompt_by_command(command: str, user=Depends(get_admin_user)):
async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)):
prompt = Prompts.get_prompt_by_command(f"/{command}")
if not prompt:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if prompt.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
result = Prompts.delete_prompt_by_command(f"/{command}")
return result
+163 -106
View File
@@ -1,54 +1,82 @@
import os
from pathlib import Path
from typing import Optional
from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
from open_webui.apps.webui.utils import load_toolkit_module_by_id, replace_imports
from open_webui.config import CACHE_DIR, DATA_DIR
from open_webui.apps.webui.models.tools import (
ToolForm,
ToolModel,
ToolResponse,
ToolUserResponse,
Tools,
)
from open_webui.apps.webui.utils import load_tools_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.utils import get_admin_user, get_verified_user
TOOLS_DIR = f"{DATA_DIR}/tools"
os.makedirs(TOOLS_DIR, exist_ok=True)
from open_webui.utils.access_control import has_access, has_permission
router = APIRouter()
############################
# GetToolkits
# GetTools
############################
@router.get("/", response_model=list[ToolResponse])
async def get_toolkits(user=Depends(get_verified_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()]
return toolkits
@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")
return tools
############################
# ExportToolKits
# GetToolList
############################
@router.get("/list", response_model=list[ToolUserResponse])
async def get_tool_list(user=Depends(get_verified_user)):
if user.role == "admin":
tools = Tools.get_tools()
else:
tools = Tools.get_tools_by_user_id(user.id, "write")
return tools
############################
# ExportTools
############################
@router.get("/export", response_model=list[ToolModel])
async def get_toolkits(user=Depends(get_admin_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()]
return toolkits
async def export_tools(user=Depends(get_admin_user)):
tools = Tools.get_tools()
return tools
############################
# CreateNewToolKit
# CreateNewTools
############################
@router.post("/create", response_model=Optional[ToolResponse])
async def create_new_toolkit(
async def create_new_tools(
request: Request,
form_data: ToolForm,
user=Depends(get_admin_user),
user=Depends(get_verified_user),
):
if user.role != "admin" and not has_permission(
user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -57,30 +85,30 @@ async def create_new_toolkit(
form_data.id = form_data.id.lower()
toolkit = Tools.get_tool_by_id(form_data.id)
if toolkit is None:
tools = Tools.get_tool_by_id(form_data.id)
if tools is None:
try:
form_data.content = replace_imports(form_data.content)
toolkit_module, frontmatter = load_toolkit_module_by_id(
tools_module, frontmatter = load_tools_module_by_id(
form_data.id, content=form_data.content
)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[form_data.id] = toolkit_module
TOOLS[form_data.id] = tools_module
specs = get_tools_specs(TOOLS[form_data.id])
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
tools = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
tool_cache_dir.mkdir(parents=True, exist_ok=True)
if toolkit:
return toolkit
if tools:
return tools
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
)
except Exception as e:
print(e)
@@ -96,16 +124,21 @@ async def create_new_toolkit(
############################
# GetToolkitById
# GetToolsById
############################
@router.get("/id/{id}", response_model=Optional[ToolModel])
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
toolkit = Tools.get_tool_by_id(id)
async def get_tools_by_id(id: str, user=Depends(get_verified_user)):
tools = Tools.get_tool_by_id(id)
if toolkit:
return toolkit
if tools:
if (
user.role == "admin"
or tools.user_id == user.id
or has_access(user.id, "read", tools.access_control)
):
return tools
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -114,26 +147,39 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
############################
# UpdateToolkitById
# UpdateToolsById
############################
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
async def update_toolkit_by_id(
async def update_tools_by_id(
request: Request,
id: str,
form_data: ToolForm,
user=Depends(get_admin_user),
user=Depends(get_verified_user),
):
tools = Tools.get_tool_by_id(id)
if not tools:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if tools.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
try:
form_data.content = replace_imports(form_data.content)
toolkit_module, frontmatter = load_toolkit_module_by_id(
tools_module, frontmatter = load_tools_module_by_id(
id, content=form_data.content
)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[id] = toolkit_module
TOOLS[id] = tools_module
specs = get_tools_specs(TOOLS[id])
@@ -143,14 +189,14 @@ async def update_toolkit_by_id(
}
print(updated)
toolkit = Tools.update_tool_by_id(id, updated)
tools = Tools.update_tool_by_id(id, updated)
if toolkit:
return toolkit
if tools:
return tools
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
detail=ERROR_MESSAGES.DEFAULT("Error updating tools"),
)
except Exception as e:
@@ -161,14 +207,28 @@ async def update_toolkit_by_id(
############################
# DeleteToolkitById
# DeleteToolsById
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
result = Tools.delete_tool_by_id(id)
async def delete_tools_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
tools = Tools.get_tool_by_id(id)
if not tools:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if tools.user_id != user.id and user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
result = Tools.delete_tool_by_id(id)
if result:
TOOLS = request.app.state.TOOLS
if id in TOOLS:
@@ -183,9 +243,9 @@ async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)):
tools = Tools.get_tool_by_id(id)
if tools:
try:
valves = Tools.get_tool_valves_by_id(id)
return valves
@@ -207,19 +267,19 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
async def get_toolkit_valves_spec_by_id(
request: Request, id: str, user=Depends(get_admin_user)
async def get_tools_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
tools = Tools.get_tool_by_id(id)
if tools:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
tools_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
tools_module, _ = load_tools_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(toolkit_module, "Valves"):
Valves = toolkit_module.Valves
if hasattr(tools_module, "Valves"):
Valves = tools_module.Valves
return Valves.schema()
return None
else:
@@ -235,42 +295,39 @@ async def get_toolkit_valves_spec_by_id(
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
async def update_toolkit_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
async def update_tools_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
Valves = toolkit_module.Valves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
valves = Valves(**form_data)
Tools.update_tool_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
tools = Tools.get_tool_by_id(id)
if not tools:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if not hasattr(tools_module, "Valves"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
Valves = tools_module.Valves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
valves = Valves(**form_data)
Tools.update_tool_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
############################
@@ -279,9 +336,9 @@ async def update_toolkit_valves_by_id(
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user)):
tools = Tools.get_tool_by_id(id)
if tools:
try:
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
return user_valves
@@ -298,19 +355,19 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
async def get_toolkit_user_valves_spec_by_id(
async def get_tools_user_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
tools = Tools.get_tool_by_id(id)
if tools:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
tools_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
tools_module, _ = load_tools_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(toolkit_module, "UserValves"):
UserValves = toolkit_module.UserValves
if hasattr(tools_module, "UserValves"):
UserValves = tools_module.UserValves
return UserValves.schema()
return None
else:
@@ -321,20 +378,20 @@ async def get_toolkit_user_valves_spec_by_id(
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
async def update_toolkit_user_valves_by_id(
async def update_tools_user_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
tools = Tools.get_tool_by_id(id)
if toolkit:
if tools:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
tools_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
tools_module, _ = load_tools_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(toolkit_module, "UserValves"):
UserValves = toolkit_module.UserValves
if hasattr(tools_module, "UserValves"):
UserValves = tools_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
+41 -4
View File
@@ -31,21 +31,58 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
return Users.get_users(skip, limit)
############################
# User Groups
############################
@router.get("/groups")
async def get_user_groups(user=Depends(get_verified_user)):
return Users.get_user_groups(user.id)
############################
# User Permissions
############################
@router.get("/permissions/user")
@router.get("/permissions")
async def get_user_permissisions(user=Depends(get_verified_user)):
return Users.get_user_groups(user.id)
############################
# User Default Permissions
############################
class WorkspacePermissions(BaseModel):
models: bool
knowledge: bool
prompts: bool
tools: bool
class ChatPermissions(BaseModel):
file_upload: bool
delete: bool
edit: bool
temporary: bool
class UserPermissions(BaseModel):
workspace: WorkspacePermissions
chat: ChatPermissions
@router.get("/default/permissions")
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
return request.app.state.config.USER_PERMISSIONS
@router.post("/permissions/user")
@router.post("/default/permissions")
async def update_user_permissions(
request: Request, form_data: dict, user=Depends(get_admin_user)
request: Request, form_data: UserPermissions, user=Depends(get_admin_user)
):
request.app.state.config.USER_PERMISSIONS = form_data
request.app.state.config.USER_PERMISSIONS = form_data.model_dump()
return request.app.state.config.USER_PERMISSIONS
+14 -55
View File
@@ -1,16 +1,14 @@
import site
from pathlib import Path
import black
import markdown
from open_webui.apps.webui.models.chats import ChatTitleMessagesForm
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
from open_webui.env import FONTS_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fpdf import FPDF
from pydantic import BaseModel
from starlette.responses import FileResponse
from open_webui.utils.misc import get_gravatar_url
from open_webui.utils.pdf_generator import PDFGenerator
from open_webui.utils.utils import get_admin_user
router = APIRouter()
@@ -56,58 +54,19 @@ class ChatForm(BaseModel):
@router.post("/pdf")
async def download_chat_as_pdf(
form_data: ChatForm,
form_data: ChatTitleMessagesForm,
):
global FONTS_DIR
try:
pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()
pdf = FPDF()
pdf.add_page()
# When running using `pip install` the static directory is in the site packages.
if not FONTS_DIR.exists():
FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
# 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")
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"])
pdf.set_auto_page_break(auto=True, margin=15)
# Adjust the effective page width for multi_cell
effective_page_width = (
pdf.w - 2 * pdf.l_margin - 10
) # Subtracted an additional 10 for extra padding
# Add chat messages
for message in form_data.messages:
role = message["role"]
content = message["content"]
pdf.set_font("NotoSans", "B", size=14) # Bold for the role
pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
pdf.ln(1) # Extra space between messages
pdf.set_font("NotoSans", size=10) # Regular for content
pdf.multi_cell(effective_page_width, 6, content, 0, "L")
pdf.ln(1.5) # Extra space between messages
# Save the pdf with name .pdf
pdf_bytes = pdf.output()
return Response(
content=bytes(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": "attachment;filename=chat.pdf"},
)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": "attachment;filename=chat.pdf"},
)
except Exception as e:
print(e)
raise HTTPException(status_code=400, detail=str(e))
@router.get("/db/download")
+12 -8
View File
@@ -5,10 +5,14 @@ import sys
from importlib import util
import types
import tempfile
import logging
from open_webui.env import SRC_LOG_LEVELS
from open_webui.apps.webui.models.functions import Functions
from open_webui.apps.webui.models.tools import Tools
from open_webui.config import FUNCTIONS_DIR, TOOLS_DIR
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def extract_frontmatter(content):
@@ -64,7 +68,7 @@ def replace_imports(content):
return content
def load_toolkit_module_by_id(toolkit_id, content=None):
def load_tools_module_by_id(toolkit_id, content=None):
if content is None:
tool = Tools.get_tool_by_id(toolkit_id)
@@ -96,7 +100,7 @@ def load_toolkit_module_by_id(toolkit_id, content=None):
# Executing the modified content in the created module's namespace
exec(content, module.__dict__)
frontmatter = extract_frontmatter(content)
print(f"Loaded module: {module.__name__}")
log.info(f"Loaded module: {module.__name__}")
# Create and return the object if the class 'Tools' is found in the module
if hasattr(module, "Tools"):
@@ -104,7 +108,7 @@ def load_toolkit_module_by_id(toolkit_id, content=None):
else:
raise Exception("No Tools class found in the module")
except Exception as e:
print(f"Error loading module: {toolkit_id}: {e}")
log.error(f"Error loading module: {toolkit_id}: {e}")
del sys.modules[module_name] # Clean up
raise e
finally:
@@ -140,7 +144,7 @@ def load_function_module_by_id(function_id, content=None):
# Execute the modified content in the created module's namespace
exec(content, module.__dict__)
frontmatter = extract_frontmatter(content)
print(f"Loaded module: {module.__name__}")
log.info(f"Loaded module: {module.__name__}")
# Create appropriate object based on available class type in the module
if hasattr(module, "Pipe"):
@@ -152,7 +156,7 @@ def load_function_module_by_id(function_id, content=None):
else:
raise Exception("No Function class found in the module")
except Exception as e:
print(f"Error loading module: {function_id}: {e}")
log.error(f"Error loading module: {function_id}: {e}")
del sys.modules[module_name] # Cleanup by removing the module in case of error
Functions.update_function_by_id(function_id, {"is_active": False})
@@ -165,7 +169,7 @@ def install_frontmatter_requirements(requirements):
if requirements:
req_list = [req.strip() for req in requirements.split(",")]
for req in req_list:
print(f"Installing requirement: {req}")
log.info(f"Installing requirement: {req}")
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
else:
print("No requirements found in frontmatter.")
log.info("No requirements found in frontmatter.")
+379 -97
View File
@@ -20,6 +20,7 @@ from open_webui.env import (
WEBUI_FAVICON_URL,
WEBUI_NAME,
log,
DATABASE_URL,
)
from pydantic import BaseModel
from sqlalchemy import JSON, Column, DateTime, Integer, func
@@ -87,6 +88,12 @@ def save_to_db(data):
db.commit()
def reset_config():
with get_db() as db:
db.query(Config).delete()
db.commit()
# When initializing, check if config.json exists and migrate it to the database
if os.path.exists(f"{DATA_DIR}/config.json"):
data = load_json_config()
@@ -258,6 +265,13 @@ class AppConfig:
# WEBUI_AUTH (Required for security)
####################################
ENABLE_API_KEY = PersistentConfig(
"ENABLE_API_KEY",
"auth.api_key.enable",
os.environ.get("ENABLE_API_KEY", "True").lower() == "true",
)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
@@ -377,7 +391,7 @@ OAUTH_USERNAME_CLAIM = PersistentConfig(
)
OAUTH_PICTURE_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"OAUTH_PICTURE_CLAIM",
"oauth.oidc.avatar_claim",
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
)
@@ -388,6 +402,33 @@ OAUTH_EMAIL_CLAIM = PersistentConfig(
os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
)
ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig(
"ENABLE_OAUTH_ROLE_MANAGEMENT",
"oauth.enable_role_mapping",
os.environ.get("ENABLE_OAUTH_ROLE_MANAGEMENT", "False").lower() == "true",
)
OAUTH_ROLES_CLAIM = PersistentConfig(
"OAUTH_ROLES_CLAIM",
"oauth.roles_claim",
os.environ.get("OAUTH_ROLES_CLAIM", "roles"),
)
OAUTH_ALLOWED_ROLES = PersistentConfig(
"OAUTH_ALLOWED_ROLES",
"oauth.allowed_roles",
[
role.strip()
for role in os.environ.get("OAUTH_ALLOWED_ROLES", "user,admin").split(",")
],
)
OAUTH_ADMIN_ROLES = PersistentConfig(
"OAUTH_ADMIN_ROLES",
"oauth.admin_roles",
[role.strip() for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(",")],
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
@@ -500,6 +541,18 @@ if CUSTOM_NAME:
pass
####################################
# STORAGE PROVIDER
####################################
STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "") # defaults to local, s3
S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None)
S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None)
S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
####################################
# File Upload DIR
####################################
@@ -515,35 +568,10 @@ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
CACHE_DIR = f"{DATA_DIR}/cache"
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Docs DIR
####################################
DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Tools DIR
####################################
TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Functions DIR
####################################
FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# OLLAMA_BASE_URL
####################################
ENABLE_OLLAMA_API = PersistentConfig(
"ENABLE_OLLAMA_API",
"ollama.enable",
@@ -555,16 +583,6 @@ OLLAMA_API_BASE_URL = os.environ.get(
)
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
if AIOHTTP_CLIENT_TIMEOUT == "":
AIOHTTP_CLIENT_TIMEOUT = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
K8S_FLAG = os.environ.get("K8S_FLAG", "")
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
@@ -596,6 +614,12 @@ OLLAMA_BASE_URLS = PersistentConfig(
"OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
)
OLLAMA_API_CONFIGS = PersistentConfig(
"OLLAMA_API_CONFIGS",
"ollama.api_configs",
{},
)
####################################
# OPENAI_API
####################################
@@ -636,15 +660,20 @@ OPENAI_API_BASE_URLS = PersistentConfig(
"OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
)
OPENAI_API_KEY = ""
OPENAI_API_CONFIGS = PersistentConfig(
"OPENAI_API_CONFIGS",
"openai.api_configs",
{},
)
# Get the actual OpenAI API key based on the base URL
OPENAI_API_KEY = ""
try:
OPENAI_API_KEY = OPENAI_API_KEYS.value[
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
]
except Exception:
pass
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
####################################
@@ -711,18 +740,48 @@ DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
],
)
MODEL_ORDER_LIST = PersistentConfig(
"MODEL_ORDER_LIST",
"ui.model_order_list",
[],
)
DEFAULT_USER_ROLE = PersistentConfig(
"DEFAULT_USER_ROLE",
"ui.default_user_role",
os.getenv("DEFAULT_USER_ROLE", "pending"),
)
USER_PERMISSIONS_CHAT_DELETION = (
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
== "true"
)
USER_PERMISSIONS_CHAT_EDITING = (
os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true"
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS", "False").lower()
== "true"
)
USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS", "False").lower()
== "true"
)
USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
)
USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_DELETE = (
os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_EDIT = (
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_TEMPORARY = (
@@ -731,28 +790,45 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
USER_PERMISSIONS = PersistentConfig(
"USER_PERMISSIONS",
"ui.user_permissions",
"user.permissions",
{
"workspace": {
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
"knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
"prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
},
"chat": {
"deletion": USER_PERMISSIONS_CHAT_DELETION,
"editing": USER_PERMISSIONS_CHAT_EDITING,
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
}
},
},
)
ENABLE_MODEL_FILTER = PersistentConfig(
"ENABLE_MODEL_FILTER",
"model_filter.enable",
os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
"ENABLE_EVALUATION_ARENA_MODELS",
"evaluation.arena.enable",
os.environ.get("ENABLE_EVALUATION_ARENA_MODELS", "True").lower() == "true",
)
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
MODEL_FILTER_LIST = PersistentConfig(
"MODEL_FILTER_LIST",
"model_filter.list",
[model.strip() for model in MODEL_FILTER_LIST.split(";")],
EVALUATION_ARENA_MODELS = PersistentConfig(
"EVALUATION_ARENA_MODELS",
"evaluation.arena.models",
[],
)
DEFAULT_ARENA_MODEL = {
"id": "arena-model",
"name": "Arena Model",
"meta": {
"profile_image_url": "/favicon.png",
"description": "Submit your questions to anonymous AI chatbots and vote on the best response.",
"model_ids": None,
},
}
WEBHOOK_URL = PersistentConfig(
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
)
@@ -866,19 +942,62 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
)
ENABLE_SEARCH_QUERY = PersistentConfig(
"ENABLE_SEARCH_QUERY",
"task.search.enable",
os.environ.get("ENABLE_SEARCH_QUERY", "True").lower() == "true",
TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"TAGS_GENERATION_PROMPT_TEMPLATE",
"task.tags.prompt_template",
os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""),
)
ENABLE_TAGS_GENERATION = PersistentConfig(
"ENABLE_TAGS_GENERATION",
"task.tags.enable",
os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
)
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"task.search.prompt_template",
os.environ.get("SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE", ""),
ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
"ENABLE_SEARCH_QUERY_GENERATION",
"task.query.search.enable",
os.environ.get("ENABLE_SEARCH_QUERY_GENERATION", "True").lower() == "true",
)
ENABLE_RETRIEVAL_QUERY_GENERATION = PersistentConfig(
"ENABLE_RETRIEVAL_QUERY_GENERATION",
"task.query.retrieval.enable",
os.environ.get("ENABLE_RETRIEVAL_QUERY_GENERATION", "True").lower() == "true",
)
QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"QUERY_GENERATION_PROMPT_TEMPLATE",
"task.query.prompt_template",
os.environ.get("QUERY_GENERATION_PROMPT_TEMPLATE", ""),
)
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
### Guidelines:
- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic.
- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
- Today's date is: {{CURRENT_DATE}}.
- Always prioritize providing actionable and broad queries that maximize informational coverage.
### Output:
Strictly return in JSON format:
{
"queries": ["query1", "query2"]
}
### Chat History:
<chat_history>
{{MESSAGES:END:6}}
</chat_history>
"""
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
@@ -899,6 +1018,8 @@ CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "")
# Comma-separated list of header=value pairs
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
if CHROMA_HTTP_HEADERS:
@@ -914,8 +1035,26 @@ CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
# Qdrant
QDRANT_URI = os.environ.get("QDRANT_URI", None)
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
# OpenSearch
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
OPENSEARCH_SSL = os.environ.get("OPENSEARCH_SSL", True)
OPENSEARCH_CERT_VERIFY = os.environ.get("OPENSEARCH_CERT_VERIFY", False)
OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
# Pgvector
PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
raise ValueError(
"Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database."
)
####################################
# RAG
# Information Retrieval (RAG)
####################################
# RAG Content Extraction
@@ -992,17 +1131,20 @@ RAG_EMBEDDING_MODEL = PersistentConfig(
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}")
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "True").lower() == "true"
)
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
)
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
"rag.embedding_openai_batch_size",
int(os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")),
RAG_EMBEDDING_BATCH_SIZE = PersistentConfig(
"RAG_EMBEDDING_BATCH_SIZE",
"rag.embedding_batch_size",
int(
os.environ.get("RAG_EMBEDDING_BATCH_SIZE")
or os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")
),
)
RAG_RERANKING_MODEL = PersistentConfig(
@@ -1014,13 +1156,29 @@ if RAG_RERANKING_MODEL.value != "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
RAG_RERANKING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "True").lower() == "true"
)
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
)
RAG_TEXT_SPLITTER = PersistentConfig(
"RAG_TEXT_SPLITTER",
"rag.text_splitter",
os.environ.get("RAG_TEXT_SPLITTER", ""),
)
TIKTOKEN_CACHE_DIR = os.environ.get("TIKTOKEN_CACHE_DIR", f"{CACHE_DIR}/tiktoken")
TIKTOKEN_ENCODING_NAME = PersistentConfig(
"TIKTOKEN_ENCODING_NAME",
"rag.tiktoken_encoding_name",
os.environ.get("TIKTOKEN_ENCODING_NAME", "cl100k_base"),
)
CHUNK_SIZE = PersistentConfig(
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
)
@@ -1030,23 +1188,34 @@ CHUNK_OVERLAP = PersistentConfig(
int(os.environ.get("CHUNK_OVERLAP", "100")),
)
DEFAULT_RAG_TEMPLATE = """You are given a user query, some textual context and rules, all inside xml tags. You have to answer the query based on the context while respecting the rules.
DEFAULT_RAG_TEMPLATE = """### Task:
Respond to the user query using the provided context, incorporating inline citations in the format [source_id] **only when the <source_id> tag is explicitly provided** in the context.
### Guidelines:
- If you don't know the answer, clearly state that.
- If uncertain, ask the user for clarification.
- Respond in the same language as the user's query.
- If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
- If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
- **Only include inline citations using [source_id] when a <source_id> tag is explicitly provided in the context.**
- Do not cite if the <source_id> tag is not provided in the context.
- Do not use XML tags in your response.
- Ensure citations are concise and directly related to the information provided.
### Example of Citation:
If the user asks about a specific topic and the information is found in "whitepaper.pdf" with a provided <source_id>, the response should include the citation like so:
* "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]."
If no <source_id> is present, the response should omit the citation.
### Output:
Provide a clear and direct response to the user's query, including inline citations in the format [source_id] only when the <source_id> tag is present in the context.
<context>
[context]
{{CONTEXT}}
</context>
<rules>
- If you don't know, just say so.
- If you are not sure, ask for clarification.
- Answer in the same language as the user query.
- If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
- If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
- Answer directly and without using xml tags.
</rules>
<user_query>
[query]
{{QUERY}}
</user_query>
"""
@@ -1067,6 +1236,19 @@ RAG_OPENAI_API_KEY = PersistentConfig(
os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
)
RAG_OLLAMA_BASE_URL = PersistentConfig(
"RAG_OLLAMA_BASE_URL",
"rag.ollama.url",
os.getenv("RAG_OLLAMA_BASE_URL", OLLAMA_BASE_URL),
)
RAG_OLLAMA_API_KEY = PersistentConfig(
"RAG_OLLAMA_API_KEY",
"rag.ollama.key",
os.getenv("RAG_OLLAMA_API_KEY", ""),
)
ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
@@ -1126,6 +1308,12 @@ BRAVE_SEARCH_API_KEY = PersistentConfig(
os.getenv("BRAVE_SEARCH_API_KEY", ""),
)
MOJEEK_SEARCH_API_KEY = PersistentConfig(
"MOJEEK_SEARCH_API_KEY",
"rag.web.search.mojeek_search_api_key",
os.getenv("MOJEEK_SEARCH_API_KEY", ""),
)
SERPSTACK_API_KEY = PersistentConfig(
"SERPSTACK_API_KEY",
"rag.web.search.serpstack_api_key",
@@ -1156,6 +1344,12 @@ TAVILY_API_KEY = PersistentConfig(
os.getenv("TAVILY_API_KEY", ""),
)
JINA_API_KEY = PersistentConfig(
"JINA_API_KEY",
"rag.web.search.jina_api_key",
os.getenv("JINA_API_KEY", ""),
)
SEARCHAPI_API_KEY = PersistentConfig(
"SEARCHAPI_API_KEY",
"rag.web.search.searchapi_api_key",
@@ -1168,6 +1362,21 @@ SEARCHAPI_ENGINE = PersistentConfig(
os.getenv("SEARCHAPI_ENGINE", ""),
)
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
"BING_SEARCH_V7_ENDPOINT",
"rag.web.search.bing_search_v7_endpoint",
os.environ.get(
"BING_SEARCH_V7_ENDPOINT", "https://api.bing.microsoft.com/v7.0/search"
),
)
BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig(
"BING_SEARCH_V7_SUBSCRIPTION_KEY",
"rag.web.search.bing_search_v7_subscription_key",
os.environ.get("BING_SEARCH_V7_SUBSCRIPTION_KEY", ""),
)
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
"RAG_WEB_SEARCH_RESULT_COUNT",
"rag.web.search.result_count",
@@ -1181,17 +1390,6 @@ RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
)
####################################
# Transcribe
####################################
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
WHISPER_MODEL_AUTO_UPDATE = (
os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
)
####################################
# Images
####################################
@@ -1230,7 +1428,7 @@ AUTOMATIC1111_CFG_SCALE = PersistentConfig(
AUTOMATIC1111_SAMPLER = PersistentConfig(
"AUTOMATIC1111_SAMPLERE",
"AUTOMATIC1111_SAMPLER",
"image_generation.automatic1111.sampler",
(
os.environ.get("AUTOMATIC1111_SAMPLER")
@@ -1407,6 +1605,19 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
# Audio
####################################
# Transcription
WHISPER_MODEL = PersistentConfig(
"WHISPER_MODEL",
"audio.stt.whisper_model",
os.getenv("WHISPER_MODEL", "base"),
)
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
WHISPER_MODEL_AUTO_UPDATE = (
os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
)
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_STT_OPENAI_API_BASE_URL",
"audio.stt.openai.api_base_url",
@@ -1428,7 +1639,7 @@ AUDIO_STT_ENGINE = PersistentConfig(
AUDIO_STT_MODEL = PersistentConfig(
"AUDIO_STT_MODEL",
"audio.stt.model",
os.getenv("AUDIO_STT_MODEL", "whisper-1"),
os.getenv("AUDIO_STT_MODEL", ""),
)
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
@@ -1486,3 +1697,74 @@ AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3"
),
)
####################################
# LDAP
####################################
ENABLE_LDAP = PersistentConfig(
"ENABLE_LDAP",
"ldap.enable",
os.environ.get("ENABLE_LDAP", "false").lower() == "true",
)
LDAP_SERVER_LABEL = PersistentConfig(
"LDAP_SERVER_LABEL",
"ldap.server.label",
os.environ.get("LDAP_SERVER_LABEL", "LDAP Server"),
)
LDAP_SERVER_HOST = PersistentConfig(
"LDAP_SERVER_HOST",
"ldap.server.host",
os.environ.get("LDAP_SERVER_HOST", "localhost"),
)
LDAP_SERVER_PORT = PersistentConfig(
"LDAP_SERVER_PORT",
"ldap.server.port",
int(os.environ.get("LDAP_SERVER_PORT", "389")),
)
LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
"LDAP_ATTRIBUTE_FOR_USERNAME",
"ldap.server.attribute_for_username",
os.environ.get("LDAP_ATTRIBUTE_FOR_USERNAME", "uid"),
)
LDAP_APP_DN = PersistentConfig(
"LDAP_APP_DN", "ldap.server.app_dn", os.environ.get("LDAP_APP_DN", "")
)
LDAP_APP_PASSWORD = PersistentConfig(
"LDAP_APP_PASSWORD",
"ldap.server.app_password",
os.environ.get("LDAP_APP_PASSWORD", ""),
)
LDAP_SEARCH_BASE = PersistentConfig(
"LDAP_SEARCH_BASE", "ldap.server.users_dn", os.environ.get("LDAP_SEARCH_BASE", "")
)
LDAP_SEARCH_FILTERS = PersistentConfig(
"LDAP_SEARCH_FILTER",
"ldap.server.search_filter",
os.environ.get("LDAP_SEARCH_FILTER", ""),
)
LDAP_USE_TLS = PersistentConfig(
"LDAP_USE_TLS",
"ldap.server.use_tls",
os.environ.get("LDAP_USE_TLS", "True").lower() == "true",
)
LDAP_CA_CERT_FILE = PersistentConfig(
"LDAP_CA_CERT_FILE",
"ldap.server.ca_cert_file",
os.environ.get("LDAP_CA_CERT_FILE", ""),
)
LDAP_CIPHERS = PersistentConfig(
"LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL")
)
+16 -2
View File
@@ -20,7 +20,9 @@ class ERROR_MESSAGES(str, Enum):
def __str__(self) -> str:
return super().__str__()
DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
DEFAULT = (
lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
)
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
@@ -34,8 +36,8 @@ class ERROR_MESSAGES(str, Enum):
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
INVALID_TOKEN = (
"Your session has expired or the token is invalid. Please sign in again."
)
@@ -60,6 +62,7 @@ class ERROR_MESSAGES(str, Enum):
NOT_FOUND = "We could not find what you're looking for :/"
USER_NOT_FOUND = "We could not find what you're looking for :/"
API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
@@ -73,6 +76,7 @@ class ERROR_MESSAGES(str, Enum):
OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
@@ -90,6 +94,15 @@ class ERROR_MESSAGES(str, Enum):
"The Ollama API is disabled. Please enable it to use this feature."
)
FILE_TOO_LARGE = (
lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}."
)
DUPLICATE_CONTENT = (
"Duplicate content detected. Please provide unique content to proceed."
)
FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
class TASKS(str, Enum):
def __str__(self) -> str:
@@ -97,6 +110,7 @@ class TASKS(str, Enum):
DEFAULT = lambda task="": f"{task if task else 'generation'}"
TITLE_GENERATION = "title_generation"
TAGS_GENERATION = "tags_generation"
EMOJI_GENERATION = "emoji_generation"
QUERY_GENERATION = "query_generation"
FUNCTION_CALLING = "function_calling"
+97 -13
View File
@@ -195,6 +195,15 @@ CHANGELOG = changelog_json
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
####################################
# ENABLE_FORWARD_USER_INFO_HEADERS
####################################
ENABLE_FORWARD_USER_INFO_HEADERS = (
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
)
####################################
# WEBUI_BUILD_HASH
####################################
@@ -221,9 +230,17 @@ if FROM_INIT_PY:
else:
shutil.copy2(item, dest)
# Zip the data directory
shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
# Remove the old data directory
shutil.rmtree(DATA_DIR)
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"))
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
@@ -234,18 +251,6 @@ if FROM_INIT_PY:
).resolve()
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
if RESET_CONFIG_ON_START:
try:
os.remove(f"{DATA_DIR}/config.json")
with open(f"{DATA_DIR}/config.json", "w") as f:
f.write("{}")
except Exception:
pass
####################################
# Database
####################################
@@ -264,6 +269,55 @@ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
if DATABASE_POOL_SIZE == "":
DATABASE_POOL_SIZE = 0
else:
try:
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
except Exception:
DATABASE_POOL_SIZE = 0
DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
if DATABASE_POOL_MAX_OVERFLOW == "":
DATABASE_POOL_MAX_OVERFLOW = 0
else:
try:
DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
except Exception:
DATABASE_POOL_MAX_OVERFLOW = 0
DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30)
if DATABASE_POOL_TIMEOUT == "":
DATABASE_POOL_TIMEOUT = 30
else:
try:
DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
except Exception:
DATABASE_POOL_TIMEOUT = 30
DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600)
if DATABASE_POOL_RECYCLE == "":
DATABASE_POOL_RECYCLE = 3600
else:
try:
DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
except Exception:
DATABASE_POOL_RECYCLE = 3600
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
####################################
# REDIS
####################################
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
####################################
# WEBUI_AUTH (Required for security)
@@ -306,4 +360,34 @@ ENABLE_WEBSOCKET_SUPPORT = (
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", "redis://localhost:6379/0")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
if AIOHTTP_CLIENT_TIMEOUT == "":
AIOHTTP_CLIENT_TIMEOUT = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "3"
)
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
)
except Exception:
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 3
####################################
# OFFLINE_MODE
####################################
OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
+878 -580
View File
File diff suppressed because it is too large Load Diff
@@ -1,19 +0,0 @@
from alembic import command
from alembic.config import Config
from open_webui.env import OPEN_WEBUI_DIR
alembic_cfg = Config(OPEN_WEBUI_DIR / "alembic.ini")
# Set the script location dynamically
migrations_path = OPEN_WEBUI_DIR / "migrations"
alembic_cfg.set_main_option("script_location", str(migrations_path))
def revision(message: str) -> None:
command.revision(alembic_cfg, message=message, autogenerate=False)
if __name__ == "__main__":
input_message = input("Enter the revision message: ")
revision(input_message)
+6
View File
@@ -7,3 +7,9 @@ def get_existing_tables():
inspector = Inspector.from_engine(con)
tables = set(inspector.get_table_names())
return tables
def get_revision_id():
import uuid
return str(uuid.uuid4()).replace("-", "")[:12]
@@ -0,0 +1,151 @@
"""Migrate tags
Revision ID: 1af9b942657b
Revises: 242a2047eae0
Create Date: 2024-10-09 21:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, select, update, column
from sqlalchemy.engine.reflection import Inspector
import json
revision = "1af9b942657b"
down_revision = "242a2047eae0"
branch_labels = None
depends_on = None
def upgrade():
# Setup an inspection on the existing table to avoid issues
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
# Clean up potential leftover temp table from previous failures
conn.execute(sa.text("DROP TABLE IF EXISTS _alembic_tmp_tag"))
# Check if the 'tag' table exists
tables = inspector.get_table_names()
# Step 1: Modify Tag table using batch mode for SQLite support
if "tag" in tables:
# Get the current columns in the 'tag' table
columns = [col["name"] for col in inspector.get_columns("tag")]
# Get any existing unique constraints on the 'tag' table
current_constraints = inspector.get_unique_constraints("tag")
with op.batch_alter_table("tag", schema=None) as batch_op:
# Check if the unique constraint already exists
if not any(
constraint["name"] == "uq_id_user_id"
for constraint in current_constraints
):
# Create unique constraint if it doesn't exist
batch_op.create_unique_constraint("uq_id_user_id", ["id", "user_id"])
# Check if the 'data' column exists before trying to drop it
if "data" in columns:
batch_op.drop_column("data")
# Check if the 'meta' column needs to be created
if "meta" not in columns:
# Add the 'meta' column if it doesn't already exist
batch_op.add_column(sa.Column("meta", sa.JSON(), nullable=True))
tag = table(
"tag",
column("id", sa.String()),
column("name", sa.String()),
column("user_id", sa.String()),
column("meta", sa.JSON()),
)
# Step 2: Migrate tags
conn = op.get_bind()
result = conn.execute(sa.select(tag.c.id, tag.c.name, tag.c.user_id))
tag_updates = {}
for row in result:
new_id = row.name.replace(" ", "_").lower()
tag_updates[row.id] = new_id
for tag_id, new_tag_id in tag_updates.items():
print(f"Updating tag {tag_id} to {new_tag_id}")
if new_tag_id == "pinned":
# delete tag
delete_stmt = sa.delete(tag).where(tag.c.id == tag_id)
conn.execute(delete_stmt)
else:
# Check if the new_tag_id already exists in the database
existing_tag_query = sa.select(tag.c.id).where(tag.c.id == new_tag_id)
existing_tag_result = conn.execute(existing_tag_query).fetchone()
if existing_tag_result:
# Handle duplicate case: the new_tag_id already exists
print(
f"Tag {new_tag_id} already exists. Removing current tag with ID {tag_id} to avoid duplicates."
)
# Option 1: Delete the current tag if an update to new_tag_id would cause duplication
delete_stmt = sa.delete(tag).where(tag.c.id == tag_id)
conn.execute(delete_stmt)
else:
update_stmt = sa.update(tag).where(tag.c.id == tag_id)
update_stmt = update_stmt.values(id=new_tag_id)
conn.execute(update_stmt)
# Add columns `pinned` and `meta` to 'chat'
op.add_column("chat", sa.Column("pinned", sa.Boolean(), nullable=True))
op.add_column(
"chat", sa.Column("meta", sa.JSON(), nullable=False, server_default="{}")
)
chatidtag = table(
"chatidtag", column("chat_id", sa.String()), column("tag_name", sa.String())
)
chat = table(
"chat",
column("id", sa.String()),
column("pinned", sa.Boolean()),
column("meta", sa.JSON()),
)
# Fetch existing tags
conn = op.get_bind()
result = conn.execute(sa.select(chatidtag.c.chat_id, chatidtag.c.tag_name))
chat_updates = {}
for row in result:
chat_id = row.chat_id
tag_name = row.tag_name.replace(" ", "_").lower()
if tag_name == "pinned":
# Specifically handle 'pinned' tag
if chat_id not in chat_updates:
chat_updates[chat_id] = {"pinned": True, "meta": {}}
else:
chat_updates[chat_id]["pinned"] = True
else:
if chat_id not in chat_updates:
chat_updates[chat_id] = {"pinned": False, "meta": {"tags": [tag_name]}}
else:
tags = chat_updates[chat_id]["meta"].get("tags", [])
tags.append(tag_name)
chat_updates[chat_id]["meta"]["tags"] = list(set(tags))
# Update chats based on accumulated changes
for chat_id, updates in chat_updates.items():
update_stmt = sa.update(chat).where(chat.c.id == chat_id)
update_stmt = update_stmt.values(
meta=updates.get("meta", {}), pinned=updates.get("pinned", False)
)
conn.execute(update_stmt)
pass
def downgrade():
pass
@@ -0,0 +1,107 @@
"""Update chat table
Revision ID: 242a2047eae0
Revises: 6a39f3d8e55c
Create Date: 2024-10-09 21:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, select, update
import json
revision = "242a2047eae0"
down_revision = "6a39f3d8e55c"
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = inspector.get_columns("chat")
column_dict = {col["name"]: col for col in columns}
chat_column = column_dict.get("chat")
old_chat_exists = "old_chat" in column_dict
if chat_column:
if isinstance(chat_column["type"], sa.Text):
print("Converting 'chat' column to JSON")
if old_chat_exists:
print("Dropping old 'old_chat' column")
op.drop_column("chat", "old_chat")
# Step 1: Rename current 'chat' column to 'old_chat'
print("Renaming 'chat' column to 'old_chat'")
op.alter_column(
"chat", "chat", new_column_name="old_chat", existing_type=sa.Text()
)
# Step 2: Add new 'chat' column of type JSON
print("Adding new 'chat' column of type JSON")
op.add_column("chat", sa.Column("chat", sa.JSON(), nullable=True))
else:
# If the column is already JSON, no need to do anything
pass
# Step 3: Migrate data from 'old_chat' to 'chat'
chat_table = table(
"chat",
sa.Column("id", sa.String(), primary_key=True),
sa.Column("old_chat", sa.Text()),
sa.Column("chat", sa.JSON()),
)
# - Selecting all data from the table
connection = op.get_bind()
results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat))
for row in results:
try:
# Convert text JSON to actual JSON object, assuming the text is in JSON format
json_data = json.loads(row.old_chat)
except json.JSONDecodeError:
json_data = None # Handle cases where the text cannot be converted to JSON
connection.execute(
sa.update(chat_table)
.where(chat_table.c.id == row.id)
.values(chat=json_data)
)
# Step 4: Drop 'old_chat' column
print("Dropping 'old_chat' column")
op.drop_column("chat", "old_chat")
def downgrade():
# Step 1: Add 'old_chat' column back as Text
op.add_column("chat", sa.Column("old_chat", sa.Text(), nullable=True))
# Step 2: Convert 'chat' JSON data back to text and store in 'old_chat'
chat_table = table(
"chat",
sa.Column("id", sa.String(), primary_key=True),
sa.Column("chat", sa.JSON()),
sa.Column("old_chat", sa.Text()),
)
connection = op.get_bind()
results = connection.execute(select(chat_table.c.id, chat_table.c.chat))
for row in results:
text_data = json.dumps(row.chat) if row.chat is not None else None
connection.execute(
sa.update(chat_table)
.where(chat_table.c.id == row.id)
.values(old_chat=text_data)
)
# Step 3: Remove the new 'chat' JSON column
op.drop_column("chat", "chat")
# Step 4: Rename 'old_chat' back to 'chat'
op.alter_column("chat", "old_chat", new_column_name="chat", existing_type=sa.Text())
@@ -0,0 +1,81 @@
"""Update tags
Revision ID: 3ab32c4b8f59
Revises: 1af9b942657b
Create Date: 2024-10-09 21:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, select, update, column
from sqlalchemy.engine.reflection import Inspector
import json
revision = "3ab32c4b8f59"
down_revision = "1af9b942657b"
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
# Inspecting the 'tag' table constraints and structure
existing_pk = inspector.get_pk_constraint("tag")
unique_constraints = inspector.get_unique_constraints("tag")
existing_indexes = inspector.get_indexes("tag")
print(f"Primary Key: {existing_pk}")
print(f"Unique Constraints: {unique_constraints}")
print(f"Indexes: {existing_indexes}")
with op.batch_alter_table("tag", schema=None) as batch_op:
# Drop existing primary key constraint if it exists
if existing_pk and existing_pk.get("constrained_columns"):
pk_name = existing_pk.get("name")
if pk_name:
print(f"Dropping primary key constraint: {pk_name}")
batch_op.drop_constraint(pk_name, type_="primary")
# Now create the new primary key with the combination of 'id' and 'user_id'
print("Creating new primary key with 'id' and 'user_id'.")
batch_op.create_primary_key("pk_id_user_id", ["id", "user_id"])
# Drop unique constraints that could conflict with the new primary key
for constraint in unique_constraints:
if (
constraint["name"] == "uq_id_user_id"
): # Adjust this name according to what is actually returned by the inspector
print(f"Dropping unique constraint: {constraint['name']}")
batch_op.drop_constraint(constraint["name"], type_="unique")
for index in existing_indexes:
if index["unique"]:
if not any(
constraint["name"] == index["name"]
for constraint in unique_constraints
):
# You are attempting to drop unique indexes
print(f"Dropping unique index: {index['name']}")
batch_op.drop_index(index["name"])
def downgrade():
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
current_pk = inspector.get_pk_constraint("tag")
with op.batch_alter_table("tag", schema=None) as batch_op:
# Drop the current primary key first, if it matches the one we know we added in upgrade
if current_pk and "pk_id_user_id" == current_pk.get("name"):
batch_op.drop_constraint("pk_id_user_id", type_="primary")
# Restore the original primary key
batch_op.create_primary_key("pk_id", ["id"])
# Since primary key on just 'id' is restored, we now add back any unique constraints if necessary
batch_op.create_unique_constraint("uq_id_user_id", ["id", "user_id"])
@@ -0,0 +1,67 @@
"""Update folder table and change DateTime to BigInteger for timestamp fields
Revision ID: 4ace53fd72c8
Revises: af906e964978
Create Date: 2024-10-23 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "4ace53fd72c8"
down_revision = "af906e964978"
branch_labels = None
depends_on = None
def upgrade():
# Perform safe alterations using batch operation
with op.batch_alter_table("folder", schema=None) as batch_op:
# Step 1: Remove server defaults for created_at and updated_at
batch_op.alter_column(
"created_at",
server_default=None, # Removing server default
)
batch_op.alter_column(
"updated_at",
server_default=None, # Removing server default
)
# Step 2: Change the column types to BigInteger for created_at
batch_op.alter_column(
"created_at",
type_=sa.BigInteger(),
existing_type=sa.DateTime(),
existing_nullable=False,
postgresql_using="extract(epoch from created_at)::bigint", # Conversion for PostgreSQL
)
# Change the column types to BigInteger for updated_at
batch_op.alter_column(
"updated_at",
type_=sa.BigInteger(),
existing_type=sa.DateTime(),
existing_nullable=False,
postgresql_using="extract(epoch from updated_at)::bigint", # Conversion for PostgreSQL
)
def downgrade():
# Downgrade: Convert columns back to DateTime and restore defaults
with op.batch_alter_table("folder", schema=None) as batch_op:
batch_op.alter_column(
"created_at",
type_=sa.DateTime(),
existing_type=sa.BigInteger(),
existing_nullable=False,
server_default=sa.func.now(), # Restoring server default on downgrade
)
batch_op.alter_column(
"updated_at",
type_=sa.DateTime(),
existing_type=sa.BigInteger(),
existing_nullable=False,
server_default=sa.func.now(), # Restoring server default on downgrade
onupdate=sa.func.now(), # Restoring onupdate behavior if it was there
)
@@ -0,0 +1,80 @@
"""Add knowledge table
Revision ID: 6a39f3d8e55c
Revises: c0fbf31ca0db
Create Date: 2024-10-01 14:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, column, select
import json
revision = "6a39f3d8e55c"
down_revision = "c0fbf31ca0db"
branch_labels = None
depends_on = None
def upgrade():
# Creating the 'knowledge' table
print("Creating knowledge table")
knowledge_table = op.create_table(
"knowledge",
sa.Column("id", sa.Text(), primary_key=True),
sa.Column("user_id", sa.Text(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=False),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
)
print("Migrating data from document table to knowledge table")
# Representation of the existing 'document' table
document_table = table(
"document",
column("collection_name", sa.String()),
column("user_id", sa.String()),
column("name", sa.String()),
column("title", sa.Text()),
column("content", sa.Text()),
column("timestamp", sa.BigInteger()),
)
# Select all from existing document table
documents = op.get_bind().execute(
select(
document_table.c.collection_name,
document_table.c.user_id,
document_table.c.name,
document_table.c.title,
document_table.c.content,
document_table.c.timestamp,
)
)
# Insert data into knowledge table from document table
for doc in documents:
op.get_bind().execute(
knowledge_table.insert().values(
id=doc.collection_name,
user_id=doc.user_id,
description=doc.name,
meta={
"legacy": True,
"document": True,
"tags": json.loads(doc.content or "{}").get("tags", []),
},
name=doc.title,
created_at=doc.timestamp,
updated_at=doc.timestamp, # using created_at for both created_at and updated_at in project
)
)
def downgrade():
op.drop_table("knowledge")
@@ -0,0 +1,85 @@
"""Add group table
Revision ID: 922e7a387820
Revises: 4ace53fd72c8
Create Date: 2024-11-14 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "922e7a387820"
down_revision = "4ace53fd72c8"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"group",
sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column("user_id", sa.Text(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("permissions", sa.JSON(), nullable=True),
sa.Column("user_ids", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
)
# Add 'access_control' column to 'model' table
op.add_column(
"model",
sa.Column("access_control", sa.JSON(), nullable=True),
)
# Add 'is_active' column to 'model' table
op.add_column(
"model",
sa.Column(
"is_active",
sa.Boolean(),
nullable=False,
server_default=sa.sql.expression.true(),
),
)
# Add 'access_control' column to 'knowledge' table
op.add_column(
"knowledge",
sa.Column("access_control", sa.JSON(), nullable=True),
)
# Add 'access_control' column to 'prompt' table
op.add_column(
"prompt",
sa.Column("access_control", sa.JSON(), nullable=True),
)
# Add 'access_control' column to 'tools' table
op.add_column(
"tool",
sa.Column("access_control", sa.JSON(), nullable=True),
)
def downgrade():
op.drop_table("group")
# Drop 'access_control' column from 'model' table
op.drop_column("model", "access_control")
# Drop 'is_active' column from 'model' table
op.drop_column("model", "is_active")
# Drop 'access_control' column from 'knowledge' table
op.drop_column("knowledge", "access_control")
# Drop 'access_control' column from 'prompt' table
op.drop_column("prompt", "access_control")
# Drop 'access_control' column from 'tools' table
op.drop_column("tool", "access_control")
@@ -0,0 +1,51 @@
"""Add feedback table
Revision ID: af906e964978
Revises: c29facfe716b
Create Date: 2024-10-20 17:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
# Revision identifiers, used by Alembic.
revision = "af906e964978"
down_revision = "c29facfe716b"
branch_labels = None
depends_on = None
def upgrade():
# ### Create feedback table ###
op.create_table(
"feedback",
sa.Column(
"id", sa.Text(), primary_key=True
), # Unique identifier for each feedback (TEXT type)
sa.Column(
"user_id", sa.Text(), nullable=True
), # ID of the user providing the feedback (TEXT type)
sa.Column(
"version", sa.BigInteger(), default=0
), # Version of feedback (BIGINT type)
sa.Column("type", sa.Text(), nullable=True), # Type of feedback (TEXT type)
sa.Column("data", sa.JSON(), nullable=True), # Feedback data (JSON type)
sa.Column(
"meta", sa.JSON(), nullable=True
), # Metadata for feedback (JSON type)
sa.Column(
"snapshot", sa.JSON(), nullable=True
), # snapshot data for feedback (JSON type)
sa.Column(
"created_at", sa.BigInteger(), nullable=False
), # Feedback creation timestamp (BIGINT representing epoch)
sa.Column(
"updated_at", sa.BigInteger(), nullable=False
), # Feedback update timestamp (BIGINT representing epoch)
)
def downgrade():
# ### Drop feedback table ###
op.drop_table("feedback")

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