Compare commits

...

809 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek b72150c881 Merge pull request #8835 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.5.7
2025-01-23 13:47:40 -08:00
Timothy Jaeryang Baek ceaefd8053 refac 2025-01-23 13:45:45 -08:00
Timothy Jaeryang Baek 2d84db2621 doc: changelog 2025-01-23 13:36:38 -08:00
Timothy Jaeryang Baek 3527023d88 refac: think tag 2025-01-23 13:04:37 -08:00
Timothy Jaeryang Baek e99453219a chore: format 2025-01-23 12:58:39 -08:00
Timothy Jaeryang Baek 45f88b80ad refac: think status 2025-01-23 12:53:17 -08:00
Timothy Jaeryang Baek ba81b47800 refac 2025-01-23 12:06:25 -08:00
Timothy Jaeryang Baek 646671c576 Merge pull request #8837 from TiancongLx/dev
i18n: update zh-TW
2025-01-23 11:23:40 -08:00
Tiancong Li 3d5e361a21 i18n: update zh-TW 2025-01-24 03:13:10 +08:00
Timothy Jaeryang Baek bb45d35a36 fix: ollama prefix id 2025-01-23 11:04:50 -08:00
Timothy Jaeryang Baek 6a9978dd12 Merge pull request #8801 from aleixdorca/dev
i18n: Update catalan translation.json
2025-01-23 10:53:24 -08:00
Timothy Jaeryang Baek 1dd8e5fafd Merge pull request #8821 from panda44312/patch-3
i18n - Update zh-cn Updated translation
2025-01-23 10:52:26 -08:00
Timothy Jaeryang Baek dd6de749d5 fix: model update access 2025-01-23 10:40:49 -08:00
Timothy Jaeryang Baek 8fc5532e2f fix: tools valves access 2025-01-23 10:37:44 -08:00
Panda 863f73de20 Update zh-cn 2025-01-23 18:44:52 +01:00
Aleix Dorca 6f17c29d93 Update catalan translation.json 2025-01-23 08:39:02 +01:00
Timothy Jaeryang Baek 6c8d68b6fc Merge pull request #8793 from open-webui/dev
fix: model ids not being applied
2025-01-22 19:01:31 -08:00
Timothy Jaeryang Baek 4a2792b4da fix: model ids not being applied 2025-01-22 19:00:46 -08:00
Timothy Jaeryang Baek 9dd45ddf7c Merge pull request #8779 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
fix: changelog
2025-01-22 14:13:56 -08:00
Timothy Jaeryang Baek dcc57500dd fix: changelog 2025-01-22 14:13:06 -08:00
Timothy Jaeryang Baek e3ae30e42f Merge pull request #8776 from open-webui/dev
refac: image gen
2025-01-22 12:49:53 -08:00
Timothy Jaeryang Baek 14e650077a refac 2025-01-22 12:49:29 -08:00
Timothy Jaeryang Baek 36b9bcee0f Merge pull request #8770 from open-webui/dev
0.5.6
2025-01-22 12:47:05 -08:00
Timothy Jaeryang Baek 6186dec781 doc: changelog 2025-01-22 12:43:46 -08:00
Timothy Jaeryang Baek 667928efb0 chore: format 2025-01-22 12:38:42 -08:00
Timothy Jaeryang Baek f4b5039adf enh: new ollama create model method 2025-01-22 12:36:18 -08:00
Timothy Jaeryang Baek 8380d3f5ae refac 2025-01-22 12:19:11 -08:00
Timothy Jaeryang Baek 419005a578 refac: manage ollama 2025-01-22 12:19:01 -08:00
Timothy Jaeryang Baek 7593d11b97 refac: wording 2025-01-22 12:08:08 -08:00
Timothy Jaeryang Baek aaf97b85dc enh: reasoning_effort param support for openai 2025-01-22 12:07:04 -08:00
Timothy Jaeryang Baek ea7d4ec6ea refac: styling 2025-01-22 11:56:39 -08:00
Timothy Jaeryang Baek 568dbc545c Merge pull request #8351 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.5.5
2025-01-22 11:34:03 -08:00
Timothy Jaeryang Baek b641756020 refac: disable duplicate attachments 2025-01-22 11:28:54 -08:00
Timothy Jaeryang Baek c6660f1d7c refac: favicon 2025-01-22 11:27:03 -08:00
Timothy Jaeryang Baek 195e632ed4 chore: logo 2025-01-22 11:19:42 -08:00
Timothy Jaeryang Baek b7f743d24c refac: favicon styling 2025-01-22 11:11:12 -08:00
Timothy Jaeryang Baek 1080ecd22c refac 2025-01-22 11:06:06 -08:00
Timothy Jaeryang Baek b5bce9cbea fix: file attachment 2025-01-22 11:04:12 -08:00
Timothy Jaeryang Baek 647195c578 chore: format 2025-01-22 10:51:29 -08:00
Timothy Jaeryang Baek ab4372600c chore: bump duckduckgo-search 2025-01-22 10:46:57 -08:00
Timothy Jaeryang Baek 8114223b54 infra: disable integration test 2025-01-22 10:43:35 -08:00
Timothy Jaeryang Baek a1a658d55b chore: dependecies 2025-01-22 10:43:08 -08:00
Timothy Jaeryang Baek 6eb4c0aecd doc: changelog 2025-01-22 10:40:48 -08:00
Timothy Jaeryang Baek 8b998420fe refac 2025-01-22 10:12:09 -08:00
Timothy Jaeryang Baek c2b5200663 refac 2025-01-22 09:42:40 -08:00
Timothy Jaeryang Baek ed7db1dd41 enh: 'notification' event type 2025-01-22 09:38:44 -08:00
Timothy Jaeryang Baek a1e4e7c007 refac: omit reasoning content in payload 2025-01-22 09:29:26 -08:00
Timothy Jaeryang Baek 9feed97f22 refac: think tag 2025-01-22 09:24:40 -08:00
Timothy Jaeryang Baek cf470e70e2 Merge pull request #8743 from open-webui/dependabot/npm_and_yarn/npm_and_yarn-37a58caa2a
build(deps): bump the npm_and_yarn group across 1 directory with 2 updates
2025-01-22 01:10:21 -08:00
Timothy Jaeryang Baek 2b1c2942a8 refac: message timestamp 2025-01-22 01:02:52 -08:00
Timothy Jaeryang Baek 10ffbca34b refac: more reasoning tags support 2025-01-22 00:56:55 -08:00
Timothy Jaeryang Baek d42f811a8d refac: think tag 2025-01-22 00:49:41 -08:00
Timothy Jaeryang Baek 039d685547 refac 2025-01-22 00:32:09 -08:00
Timothy Jaeryang Baek 2777d3ec49 refac: think tag styling 2025-01-22 00:23:47 -08:00
Timothy Jaeryang Baek c9dc7299c5 enh: <think> tag support 2025-01-22 00:13:24 -08:00
dependabot[bot] 0e6c680db8 build(deps): bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [katex](https://github.com/KaTeX/KaTeX) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `katex` from 0.16.10 to 0.16.21
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.10...v0.16.21)

Updates `vite` from 5.4.6 to 5.4.14
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.14/packages/vite)

---
updated-dependencies:
- dependency-name: katex
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 06:16:45 +00:00
Timothy Jaeryang Baek 8d3c73aed5 chore: format backend 2025-01-21 19:33:33 -08:00
Timothy Jaeryang Baek 4c86f39a84 chore: format 2025-01-21 19:33:26 -08:00
Timothy Jaeryang Baek 35547c4246 Merge pull request #8736 from aqiray/dev
i18n: Update Turkish Translation
2025-01-21 19:32:20 -08:00
Giray a839808991 i18n: Update Turkish Translation 2025-01-22 05:29:46 +03:00
Timothy Jaeryang Baek 80e004c31f Merge pull request #8637 from kahghi/add-gcs-storage-provider
feat:add GCSStorageProvider
2025-01-21 18:14:22 -08:00
Timothy Jaeryang Baek ffd10e8b6f Merge pull request #8712 from rragundez/improve-s3-provider-tests
Improve s3 provider tests
2025-01-21 11:35:46 -08:00
Timothy Jaeryang Baek 8cca31e6fe Merge pull request #8711 from KarlLee830/translate
i18n: Update Chinese Translation
2025-01-21 11:34:25 -08:00
Rodrigo Agundez bdc6082221 Move patchin of S3 obects inside the init 2025-01-21 17:21:54 +08:00
kahghi a93124cafb tidied up test fixture for gcp storage emulator 2025-01-21 17:19:56 +08:00
KarlLee830 b2178b982d i18n: Update Chinese Translation 2025-01-21 15:50:16 +08:00
Timothy Jaeryang Baek 6408452e8f refac: model editor 2025-01-20 23:44:47 -08:00
kahghi 4aa9b8d5e7 removed unused pytest-env and imports 2025-01-21 15:38:41 +08:00
Timothy Jaeryang Baek 94d0f84c0c refac: styling 2025-01-20 23:26:43 -08:00
Timothy Jaeryang Baek 45f4bc18f8 refac: access controls 2025-01-20 23:20:47 -08:00
Timothy Jaeryang Baek aa442f694b enh: validate user id before saving group 2025-01-20 23:09:55 -08:00
Timothy Jaeryang Baek 31ed1fcdb8 fix: tools permissions 2025-01-20 22:57:40 -08:00
Timothy Jaeryang Baek 7a70fd1312 fix: bing search 2025-01-20 22:52:19 -08:00
kahghi b1887fef1b added suggestions 2025-01-21 14:47:56 +08:00
Timothy Jaeryang Baek 006208f9a9 enh: title edit should auto focus to input 2025-01-20 22:44:59 -08:00
Timothy Jaeryang Baek a863f98c53 refac: toast error 2025-01-20 22:41:32 -08:00
Timothy Jaeryang Baek af48f346f1 enh: reintroduce model management to models settings 2025-01-20 22:39:00 -08:00
kahghi 55cc127b03 gcs tests pass, updated get_file and delete_file to retrieve blob instead of instantiating blob 2025-01-21 12:53:49 +08:00
kahghi 4b56c15a3f test_upload_file working, added gcp-storage-emulator, updated gcs client instantiation 2025-01-20 22:35:43 +08:00
kahghi 44574c434d removed unnecessary GCS_PROJECT_ID variable 2025-01-20 15:27:55 +08:00
Timothy Jaeryang Baek 59c349de00 refac: styling 2025-01-19 12:46:02 -08:00
Timothy Jaeryang Baek bdc60e7850 chore: format backend 2025-01-19 11:59:07 -08:00
Timothy Jaeryang Baek a1e33a82f0 chore: format 2025-01-19 11:58:58 -08:00
Timothy Jaeryang Baek 9a794b1f8f refac 2025-01-19 11:49:51 -08:00
Timothy Jaeryang Baek 149eea2377 refac: chat controls 2025-01-19 11:49:16 -08:00
Timothy Jaeryang Baek db8b3d29da refac: app styling 2025-01-19 11:48:05 -08:00
Timothy Jaeryang Baek 6b7fe4f81a Merge pull request #8683 from OriginalSimon/dev
18n: Update Ukrainian translation
2025-01-19 11:47:42 -08:00
Timothy Jaeryang Baek e240b95026 Merge pull request #8675 from jyje/i18n/ko-KR
i18n: update ko-KR for settings and admin settings
2025-01-19 11:47:01 -08:00
Simon 475568d9e7 Update translation.json 2025-01-19 19:32:16 +01:00
Simon fd3348d564 Update translation.json 2025-01-19 19:25:15 +01:00
kahghi 49f31ddcd8 added alternative storage client instantiation method, corrected filepaths, added missing type hinting 2025-01-19 22:42:29 +08:00
jyje 4fa234b6e9 i18n: fix ko-KR for awkward words 2025-01-19 19:57:08 +09:00
jyje 04b646eac0 i18n: update ko-KR for 'admin settings audio'
- admin Settings/Audio
2025-01-19 19:49:31 +09:00
jyje 5e65471560 i18n: update ko-KR for 'admin settings interface' 2025-01-19 19:40:29 +09:00
jyje a98a84859e i18n: update ko-KR for 'admin settings web search'
- admin Settings/WebSearch
2025-01-19 19:30:14 +09:00
jyje 39bc76cbc4 i18n: update ko-KR for 'admin settings documents'
- admin Settings/Documents
2025-01-19 18:59:07 +09:00
jyje 03d54f97ab i18n: update ko-KR for 'admin settings evaluations'
- Settings/Evaulations/ArenaModelModal
- Workspace/Common/AccessControl
2025-01-19 17:33:04 +09:00
jyje 568b873fff i18n: update ko-KR for admin Settings/Models, Settings/Models/ConfigureModelsModal 2025-01-19 17:14:28 +09:00
jyje 09827064fe i18n: update ko-KR for admin Settings/Connections 2025-01-19 17:08:18 +09:00
jyje c26e7820e1 i18n: update ko-KR for admin Settings/General 2025-01-19 17:00:39 +09:00
jyje 0a6ba21e57 i18n: update ko-KR for Settings/Account 2025-01-19 16:47:43 +09:00
jyje 42db2748e6 i18n: update ko-KR for Settings/General, Settings/Advanced 2025-01-19 16:37:34 +09:00
Timothy Jaeryang Baek 3fefbb9e20 refac 2025-01-18 17:50:03 -08:00
Timothy Jaeryang Baek 71ab8b9c93 enh: allow connections with duplicate urls 2025-01-18 17:22:29 -08:00
Timothy Jaeryang Baek ca0285fc91 refac: connections handling 2025-01-18 17:10:15 -08:00
Timothy Jaeryang Baek 430854e223 refac 2025-01-18 16:34:13 -08:00
Timothy Jaeryang Baek 43c4b8e518 fix: ollama connections 2025-01-18 16:28:39 -08:00
Timothy Jaeryang Baek 2b66a7a8dc Merge pull request #8670 from SlavikCA/main
docs: Update incorrect hint about `num_gpu` parameter
2025-01-18 15:42:10 -08:00
slavik.fursov c7c3c4a23c Update incorrect hint about num_gpu parameter 2025-01-18 14:22:01 -08:00
Timothy Jaeryang Baek 27f1ed18f7 Merge pull request #8668 from jyje/i18n/ko-KR
i18n: update ko-KR
2025-01-18 11:07:18 -08:00
jyje 66796b38bc i18n: update ko-KR for Settings/Interface, Messages/RateComment and etc 2025-01-19 02:11:39 +09:00
Timothy Jaeryang Baek f5437dae4a refac: electron app 2025-01-17 17:05:52 -08:00
Timothy Jaeryang Baek 70aced7f58 refac: styling 2025-01-17 17:03:25 -08:00
Timothy Jaeryang Baek 2535e1b53a Merge pull request #8659 from open-webui/dependabot/npm_and_yarn/npm_and_yarn-7064c9a8ac
build(deps): bump katex from 0.16.10 to 0.16.21 in the npm_and_yarn group across 1 directory
2025-01-17 15:25:45 -08:00
Timothy Jaeryang Baek 7519f683ed Merge pull request #8660 from jk-f5/fix/decode
Remove unnecessary decode statement from release_lock method
2025-01-17 15:25:36 -08:00
Jason Kidd c5b67ea430 fix: Remove unnecessary decode statement
Since we create our Redis instance with , we don't
need to worry about decoding this value here. Plus this doesn't work
in python3 anyway
2025-01-17 13:43:56 -08:00
dependabot[bot] 1827eaca2d build(deps): bump katex in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [katex](https://github.com/KaTeX/KaTeX).


Updates `katex` from 0.16.10 to 0.16.21
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.10...v0.16.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 21:34:21 +00:00
Timothy Jaeryang Baek 217e3a13c8 feat: Add ability to change permissions on group creation API 2025-01-17 12:03:24 -08:00
Timothy Jaeryang Baek 2818f3cf95 Merge pull request #8610 from excho0/patch-1
🌟 Chore: Add Svelte Inspector for Easier Development 🌟
2025-01-17 10:30:03 -08:00
Timothy Jaeryang Baek 3994b1c6f7 Merge pull request #8623 from Tryanks/autocompletion
feat: non-english (chinese) autocompletion handling
2025-01-17 10:28:32 -08:00
Timothy Jaeryang Baek 382dece228 Merge pull request #8638 from Tryanks/oauth2-github
refactor: Extend OIDC support to all OAuth authentication methods
2025-01-17 10:27:49 -08:00
kahghi 1764de41f3 updated to use credentials json, tested with local built image and upload to gcs works 2025-01-17 21:53:41 +08:00
kahghi 8efc543f49 added google-cloud-storage package, added gcs related functions 2025-01-17 16:16:25 +08:00
Tryanks e7971b5840 enh: add github oauth2 provider support 2025-01-17 13:22:35 +08:00
Tryanks f3e6dacf0d refac: Extend OIDC support to all OAuth authentication methods 2025-01-17 12:56:03 +08:00
kahghi 42b7beb8a7 add GCSStorageProvider 2025-01-17 09:39:59 +08:00
Timothy Jaeryang Baek 7cf4c9c89c refac: comfyui 2025-01-16 11:17:37 -08:00
Timothy Jaeryang Baek 5526c43853 Merge pull request #8620 from Tryanks/dev
fix: incorrectly indexing the key userinfo in the token.
2025-01-16 11:12:41 -08:00
Timothy Jaeryang Baek eb17cf6915 Merge pull request #8608 from rragundez/hotfix-get-picture
Hotfix get picture
2025-01-16 11:12:18 -08:00
Timothy Jaeryang Baek 4645943066 refac: file handler 2025-01-16 11:11:23 -08:00
Tryanks 612ca54afa enh: non-english (chinese) autocompletion handling 2025-01-17 01:50:23 +08:00
Tryanks 4b6700a4b2 fixed: incorrectly indexing the key userinfo in the token. 2025-01-17 00:33:20 +08:00
Excho 8468085291 Update svelte.config.js
Added svelte inspector for easier development
2025-01-16 14:29:00 +02:00
Rodrigo Agundez 91df1c56b2 Add headers 2025-01-16 19:32:35 +08:00
Timothy Jaeryang Baek 0425621494 refac 2025-01-16 00:13:02 -08:00
Timothy Jaeryang Baek 0360aa5520 enh: image prompt enhancer 2025-01-16 00:06:37 -08:00
Timothy Jaeryang Baek d3a5b9c127 refac: styling 2025-01-15 23:39:29 -08:00
Timothy Jaeryang Baek a10302d909 enh: image generation toggle 2025-01-15 23:32:13 -08:00
Timothy Jaeryang Baek 92022dd81f enh: image permissions 2025-01-15 23:12:40 -08:00
Timothy Jaeryang Baek 90d4bedae8 enh: ability to set chat controls permissions 2025-01-15 23:08:54 -08:00
Timothy Jaeryang Baek 56f57928c2 enh/refac: permissions 2025-01-15 23:01:43 -08:00
Timothy Jaeryang Baek 2aa82d98cc refac: styling 2025-01-15 21:51:11 -08:00
Timothy Jaeryang Baek 8b3fb2a8b5 Merge pull request #8580 from rragundez/split-storage-providers
Split the storage providers into separate classes in preparation for adding more cloud providers
2025-01-15 21:08:04 -08:00
Rodrigo Agundez f14467c65e Update dependencies to include mock 2025-01-16 13:00:17 +08:00
Rodrigo Agundez 544243fabb Add moto as a dependency 2025-01-16 12:37:54 +08:00
Rodrigo Agundez 021c1f3900 Add test for S3 using moto 2025-01-16 12:37:44 +08:00
Rodrigo Agundez 357e7dd20f Add tests for local provider 2025-01-16 11:13:10 +08:00
Rodrigo Agundez 3aa28de5f1 Add test classes for the providers 2025-01-16 08:31:59 +08:00
Rodrigo Agundez 535e59af8d Start testing files for provider 2025-01-16 08:10:17 +08:00
Rodrigo Agundez dd16c3d5c1 add function to retrieve the storage provider so it can be easily tested 2025-01-16 08:09:56 +08:00
Rodrigo Agundez 0129255478 Set default storage provider to local 2025-01-16 08:01:26 +08:00
Rodrigo Agundez a3f737c0c6 Split the storage providers into separate classes in preparation for other storage providers like GCS 2025-01-16 07:50:12 +08:00
Timothy Jaeryang Baek 372658be6d Merge pull request #8547 from juananpe/file-deletion
fix: File deletion doesn't properly clean up database entries
2025-01-15 10:11:29 -08:00
Timothy Jaeryang Baek d1bde9f348 Merge pull request #8562 from NYU-ITS/milvus_custom_db
[feat] Milvus: add new config var, MILVUS_DB
2025-01-15 10:10:10 -08:00
Timothy Jaeryang Baek b797112322 Merge pull request #8565 from mykola-mmm/fix/show_user_chats_from_folders
Fix: updated get_chat_list_by_user_id method to return all user cha…
2025-01-15 10:09:43 -08:00
Timothy Jaeryang Baek 3328e2ea97 Merge pull request #8566 from rragundez/ms-auth-picture
Add functionality to retrive picture for microsoft oauth
2025-01-15 10:02:28 -08:00
Timothy Jaeryang Baek 0775ddbdf5 Merge pull request #8585 from TiancongLx/dev
i18n: update zh-TW
2025-01-15 09:53:34 -08:00
Tiancong Li 6e088df173 i18n: update zh-TW 2025-01-16 01:05:26 +08:00
Timothy Jaeryang Baek 5e548fa7b8 refac: styling 2025-01-14 23:53:19 -08:00
Rodrigo Agundez 32f121f019 Add functionality to retrive picture for microsoft oauth 2025-01-15 09:18:16 +08:00
Rodrigo Agundez c8be0b20cc Add functionality to retrive picture for microsoft oauth 2025-01-15 09:17:22 +08:00
mykola b1568878e7 Fixed: updated get_chat_list_by_user_id method to return all user chats (including those moved to the folders) 2025-01-14 23:15:56 +00:00
Sajid Ali 7a95df008e Milvus: add new config var, MILVUS_DB
modified:   backend/open_webui/config.py
	modified:   backend/open_webui/retrieval/vector/dbs/milvus.py
2025-01-14 15:48:15 -05:00
Juanan Pereira f477f4f790 fix: fixess issue #7181 2025-01-14 09:32:04 +01:00
Timothy Jaeryang Baek f6a54c96bc Merge pull request #8470 from tarmst/add-read-write-toggle-to-access-control
feat: Add toggle to read/write perms on access control
2025-01-13 21:22:36 -08:00
Timothy Jaeryang Baek bb261fcc4a refac: styling 2025-01-13 21:20:31 -08:00
Timothy Jaeryang Baek 4044d80024 fix 2025-01-13 21:15:59 -08:00
Timothy Jaeryang Baek 3a3764bf68 refac: drag region 2025-01-13 21:15:22 -08:00
Timothy Jaeryang Baek c4486b4a77 refac: styling 2025-01-13 21:13:16 -08:00
Timothy Jaeryang Baek ac5bdac161 refac: styling 2025-01-13 20:21:47 -08:00
Timothy Jaeryang Baek ce1712fed1 fix: electron api 2025-01-13 19:53:00 -08:00
Timothy Jaeryang Baek 15e591f30d enh: electron window focus detect 2025-01-13 19:47:24 -08:00
Timothy Jaeryang Baek 84b7583982 refac: styling 2025-01-13 19:39:18 -08:00
Timothy Jaeryang Baek 4fcf0caa4d refac 2025-01-13 19:25:07 -08:00
Timothy Jaeryang Baek 22ad0264c1 refac: styling 2025-01-13 19:23:57 -08:00
Timothy Jaeryang Baek f299e19002 refac: styling 2025-01-13 19:18:32 -08:00
Timothy Jaeryang Baek 147eb0d2d1 refac: electron drag region 2025-01-13 18:19:46 -08:00
Timothy Jaeryang Baek 44da86895d refac: styling 2025-01-13 18:08:17 -08:00
Timothy Jaeryang Baek eb99785047 Merge pull request #8450 from d4v3y0rk/fix-integration-test-error
ci: fix for cypress error in integration test
2025-01-13 13:16:57 -08:00
Timothy Jaeryang Baek d83acb2f03 Merge pull request #8510 from MadsLang/main
Add DATABASE_SCHEMA as env var
2025-01-13 13:16:06 -08:00
Timothy Jaeryang Baek b6128efb43 chore: bump 2025-01-13 00:34:44 -08:00
Timothy Jaeryang Baek ad93341b74 refac 2025-01-13 00:34:15 -08:00
MadsLang d4a26f8031 Merge branch 'open-webui:main' into main 2025-01-13 08:28:13 +01:00
Timothy Jaeryang Baek 42e8d8ce49 Merge pull request #8447 from steelcg/dev
feat: add LDAP_ATTRIBUTE_FOR_MAIL to env-configuration
2025-01-12 18:15:28 -08:00
Timothy Jaeryang Baek 8e08c218c5 Merge pull request #8499 from juananpe/file-cleanup-knowledge
fix: complete file cleanup when removing from Knowledge Base
2025-01-12 18:14:48 -08:00
Timothy Jaeryang Baek 3e7036151e Merge pull request #8490 from flefevre/i18n-fr-FR
i18n - Updated French Translation
2025-01-12 12:03:54 -08:00
Juanan Pereira 913e0d391b fix: complete file cleanup when removing from Knowledge Base 2025-01-12 20:04:25 +01:00
Francois LE FEVRE 805e0ec7d1 i18n - Updated French Translation 2025-01-12 10:28:19 +01:00
Timothy Jaeryang Baek b1bcca90b5 refac 2025-01-11 20:23:26 -08:00
Timothy Jaeryang Baek 5d3f778b2a fix: openai proxy endpoints 2025-01-11 16:56:25 -08:00
Timothy Jaeryang Baek ccbd98dfab refac: action buttons styling 2025-01-11 13:47:02 -08:00
Timothy Jaeryang Baek 21abe43f3b refac 2025-01-10 16:27:37 -08:00
Timothy Jaeryang Baek dd08b457d8 Merge pull request #8471 from open-webui/main
Update About.svelte
2025-01-10 11:46:59 -08:00
Timothy Jaeryang Baek 4269df041f Update About.svelte 2025-01-10 11:46:35 -08:00
Timothy Jaeryang Baek 042c9fbef3 Merge pull request #8469 from open-webui/main
Update README.md
2025-01-10 11:09:14 -08:00
Timothy Jaeryang Baek 1972f33100 Update README.md 2025-01-10 11:07:45 -08:00
Timothy Jaeryang Baek 6aee001ab0 Merge pull request #8468 from open-webui/main
Update LICENSE
2025-01-10 10:55:37 -08:00
tarmst 49eca68e28 Add toggle to read/write perms on access control 2025-01-10 18:44:50 +00:00
Timothy Jaeryang Baek a76068d69c Update LICENSE 2025-01-10 10:40:39 -08:00
Timothy Jaeryang Baek cd20c578d2 Merge pull request #8449 from hurxxxx/main
fix : incorrect element id
2025-01-10 08:28:47 -08:00
Timothy Jaeryang Baek a078403c34 Merge pull request #8457 from panda44312/patch-2
i18n - zh-cn Updated translation
2025-01-10 08:28:04 -08:00
Timothy Jaeryang Baek 5ccdddff99 Merge pull request #8458 from aindriu80/main
i18n - Updated Irish translations
2025-01-10 08:27:52 -08:00
Aindriú Mac Giolla Eoin 0be703ecb7 Updating Irish translations 2025-01-10 15:11:46 +00:00
Panda 35ca4f565f Update translation.json 2025-01-10 15:48:52 +01:00
Dave York 4d349ac5c9 potential fix for cypress error 2025-01-10 01:27:00 -05:00
Gunwoo Heo a6ca6c1712 fix : incorrect element id 2025-01-10 14:41:39 +09:00
Li, Steel ab6dffffd0 feat: add LDAP_ATTRIBUTE_FOR_MAIL to env-configuration 2025-01-10 08:53:03 +08:00
Timothy Jaeryang Baek 88614ec70a Merge pull request #8432 from juananpe/kb-detachment-from-models
fix: Knowledge Base Detachment from Models
2025-01-09 10:24:57 -08:00
Juanan Pereira f72490093a fix frontend format 2025-01-09 18:56:40 +01:00
Juanan Pereira de735b3c8c fix format 2025-01-09 18:41:18 +01:00
Juanan Pereira 5b616dd34f fix: Knowledge Base Detachment from Models 2025-01-09 16:47:12 +01:00
Timothy Jaeryang Baek 942fd384de refac: chroma 2025-01-08 13:18:14 -08:00
Timothy Jaeryang Baek bd9bc4d7eb Merge pull request #8401 from TiancongLx/dev
i18n: update zh-TW
2025-01-08 11:34:48 -08:00
Timothy Jaeryang Baek 6c4b2b0a8e Merge pull request #8407 from LuisMalhadas/no_line_break_pdf(#8405)
fix: solves the lack of line breaks in chat download as pdf #8405
2025-01-08 11:34:32 -08:00
Timothy Jaeryang Baek 256b9161e6 Merge pull request #8409 from vertago1/main
**docs**: docs/apache.md: Add websocket proxy
2025-01-08 11:34:17 -08:00
Timothy Jaeryang Baek 9516eb3f32 refac: bump chromadb 2025-01-08 11:33:30 -08:00
Allen Webb 76f99d193f docs/apache.md: Add websocket proxy
After 0.5 websocket support is required so update the proxy instructions
to include websockets.

Issue #8074#issuecomment-2562017399
2025-01-08 10:13:00 -06:00
Luis Malhadas 882d8f4ce1 solves the lack of line breaks in chat download as pdf #8405 2025-01-08 16:49:53 +01:00
Tiancong Li 130a771b60 i18n: update zh-TW 2025-01-08 20:17:41 +08:00
Timothy Jaeryang Baek e6db4d017b fix 2025-01-08 00:59:03 -08:00
Timothy Jaeryang Baek 987664f9b5 fix
Co-Authored-By: Izhar Firdaus <480984+kagesenshi@users.noreply.github.com>
2025-01-08 00:57:52 -08:00
Timothy Jaeryang Baek b3c7ecaea1 fix: oauth webhook
Co-Authored-By: Izhar Firdaus <480984+kagesenshi@users.noreply.github.com>
2025-01-08 00:38:00 -08:00
Timothy Jaeryang Baek c79b975ad0 refac: chroma 2025-01-08 00:21:50 -08:00
Timothy Jaeryang Baek 656a887db6 refac 2025-01-08 00:10:38 -08:00
Timothy Jaeryang Baek e63c0aeb1b refac: styling 2025-01-08 00:03:31 -08:00
Timothy Jaeryang Baek 433168c450 fix: add user modal 2025-01-08 00:02:33 -08:00
Timothy Jaeryang Baek 0129f463b4 fix: &lt; rendering issue 2025-01-07 23:59:58 -08:00
Timothy Jaeryang Baek e0054298a4 Merge pull request #8377 from Vojtech-Siler/multiple-error-fixes
fix: Multiple Fixes for Range and Type Errors
2025-01-07 23:53:48 -08:00
Timothy Jaeryang Baek 0e7c3d4eb8 Merge pull request #8379 from qiaozhi199/main
Fix the issue of inaccurate answers after enabling RAG query generation
2025-01-07 23:53:31 -08:00
Timothy Jaeryang Baek 70914d2764 Merge pull request #8376 from steelcg/dev
perf: use ldap3.NONE for parameter get_info in ldap_auth function to accelerate login
2025-01-07 23:50:11 -08:00
Timothy Jaeryang Baek 156579ab3a fix/doc: changelog date 2025-01-07 17:19:46 -08:00
Timothy Jaeryang Baek 81d03540bd Merge pull request #8385 from jk-f5/refac/pgvectorlength
fix: Pgvector vector column size check was failing on initialization …
2025-01-07 14:35:47 -08:00
Jason Kidd b3a52be401 fix: Pgvector vector column size check was failing on initialization of database 2025-01-07 09:15:13 -08:00
zhiguo.qiao 91f22a8a8d Return the top k results with the highest similarity. 2025-01-07 17:41:30 +08:00
Vojtěch Šiler 70a6a0d9e8 Fix errors for RangeErrors, Fix for accessing undefined objects in Chat.svelte 2025-01-07 08:54:42 +01:00
Li, Steel 82ff6c371b use ldap3.NONE for parameter get_info in ldap_auth function to accelerate login 2025-01-07 14:13:18 +08:00
Timothy Jaeryang Baek 0c5bb6df80 Merge pull request #8371 from tarmst/fix-admins-added-to-groups-from-oauth-group-mgmt
fix: Fix admins added to groups from oauth group management
2025-01-06 10:30:08 -08:00
tarmst 8117bf8603 Add admin check 2025-01-06 18:23:42 +00:00
Timothy Jaeryang Baek a0e63b08c3 fix: searchapi engine not showing 2025-01-06 10:10:25 -08:00
Timothy Jaeryang Baek 960683eced fix: torch mps not working
Co-Authored-By: Rich Tong <1782087+richtong@users.noreply.github.com>
2025-01-06 10:08:12 -08:00
Timothy Jaeryang Baek 15a182c9d6 refac: styling 2025-01-05 13:58:29 -08:00
Timothy Jaeryang Baek 61303a216c refac: styling 2025-01-05 13:56:42 -08:00
Timothy Jaeryang Baek 1dfb479d36 Merge pull request #8329 from open-webui/dev
infra: disable codespell
2025-01-05 01:53:03 -08:00
Timothy Jaeryang Baek c517fa906f infra: disable codespell 2025-01-05 01:52:47 -08:00
Timothy Jaeryang Baek 506dc0149c Merge pull request #8246 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.5.4
2025-01-05 01:38:42 -08:00
Timothy Jaeryang Baek d651cdb1e4 doc: changelog 2025-01-05 01:38:34 -08:00
Timothy Jaeryang Baek 0c40319371 doc: readme 2025-01-05 01:36:27 -08:00
Timothy Jaeryang Baek c74083ace7 doc: wording 2025-01-05 01:34:31 -08:00
Timothy Jaeryang Baek a3ca39f377 refac 2025-01-05 01:33:12 -08:00
Timothy Jaeryang Baek d11e6e40b1 doc: changelog 2025-01-05 01:32:26 -08:00
Timothy Jaeryang Baek 71bbdf25c6 refac 2025-01-05 01:18:30 -08:00
Timothy Jaeryang Baek f4a595db11 chore: format 2025-01-05 01:12:19 -08:00
Timothy Jaeryang Baek c8be0ee1e0 fix: temp chat json export 2025-01-05 01:10:45 -08:00
Timothy Jaeryang Baek 6ed8c3cb70 Update pyproject.toml 2025-01-05 00:56:32 -08:00
Timothy Jaeryang Baek c434aff1a2 refac: styling 2025-01-05 00:48:41 -08:00
Timothy Jaeryang Baek 3886c82ed0 Merge pull request #8286 from srajangarg/sg
feat: functionality to clone shared chats
2025-01-05 00:45:55 -08:00
Timothy Jaeryang Baek fe59b7f39c refac 2025-01-05 00:44:38 -08:00
Timothy Jaeryang Baek a4132322d9 refac 2025-01-05 00:28:36 -08:00
Timothy Jaeryang Baek d477f73c7e enh: torch mps support 2025-01-05 00:27:44 -08:00
Timothy Jaeryang Baek 614379a427 fix: chat drag and drop 2025-01-04 02:42:26 -08:00
Timothy Jaeryang Baek b4d91b5cb4 refac 2025-01-04 02:34:12 -08:00
Timothy Jaeryang Baek f31e2af349 refac: play sound only in last active tab 2025-01-04 02:30:51 -08:00
Timothy Jaeryang Baek afcbf2cafb enh: native notifications 2025-01-04 02:15:57 -08:00
Timothy Jaeryang Baek c0b93791dc refac 2025-01-04 02:05:42 -08:00
Timothy Jaeryang Baek f3a4d61f81 fix: actions 2025-01-03 21:31:24 -08:00
Timothy Jaeryang Baek 99c3194181 fix: API_KEY_ALLOWED_ENDPOINTS 2025-01-03 13:08:21 -08:00
Timothy Jaeryang Baek 0e805e7dc4 Merge pull request #8298 from jk-f5/feat/pg_vector_size
feat: Allow setting the initial vector length on pgvector document_chunk table
2025-01-03 13:03:53 -08:00
Timothy Jaeryang Baek 226408ffe3 Merge pull request #8309 from yarikoptic/enh-codespell
ci: add codespell support (config, workflow to detect/not fix) and make it fix few typos
2025-01-03 13:02:32 -08:00
Yaroslav Halchenko 8f1953e667 [DATALAD RUNCMD] run codespell throughout fixing few left typos automagically
=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^
2025-01-03 15:07:21 -05:00
Yaroslav Halchenko 1a928bc6a4 Add rudimentary codespell config 2025-01-03 15:07:21 -05:00
Yaroslav Halchenko 2e5fbba961 Add github action to codespell main on push and PRs 2025-01-03 15:07:21 -05:00
Jason Kidd 70b74b5217 feat: Allow setting the initial vector length on pgvector document_chunk table 2025-01-03 09:18:59 -08:00
Timothy Jaeryang Baek 996ade9090 refac: notification sound behaviour 2025-01-02 21:39:40 -08:00
Srajan Garg 2444327a47 add functionality to clone shared chats 2025-01-03 00:08:49 -05:00
Timothy Jaeryang Baek 6b3be4bb5b refac: sidebar styling 2025-01-02 20:53:30 -08:00
Timothy Jaeryang Baek b2b56d14cb fix: channel thread 2025-01-02 20:48:50 -08:00
Timothy Jaeryang Baek 3e9c92729d refac: styling 2025-01-02 20:28:01 -08:00
Timothy Jaeryang Baek 0c49553f79 refac: styling 2025-01-02 20:23:07 -08:00
Timothy Jaeryang Baek 66b8166397 refac: styling 2025-01-02 20:19:52 -08:00
Timothy Jaeryang Baek 87ba39df57 fix: moa 2025-01-02 14:32:25 -08:00
Timothy Jaeryang Baek 43964de675 refac: audio input 2025-01-02 02:32:51 -08:00
Timothy Jaeryang Baek fab82b51de fix: generate_queries 2025-01-01 20:13:28 -08:00
Timothy Jaeryang Baek 44716a4cb2 refac: disable ENABLE_REALTIME_CHAT_SAVE by default 2025-01-01 20:12:42 -08:00
Timothy Jaeryang Baek d9fb971196 Merge pull request #8249 from albinvar/main
fix: raise exceptions properly in process_chat_payload
2025-01-01 18:44:42 -08:00
Timothy Jaeryang Baek 51d875a14e Merge pull request #8251 from TiancongLx/dev
i18n: update zh-TW
2025-01-01 18:44:16 -08:00
Tiancong Li 45cc0e6209 i18n: update zh-TW
待翻譯:「Add Reaction」
2025-01-01 21:20:11 +08:00
Albin Varghese c561eb7bbd fix: raise exceptions properly in process_chat_payload 2025-01-01 17:15:06 +05:30
Timothy Jaeryang Baek 00a089b596 doc: readme 2024-12-31 20:37:54 -08:00
Timothy Jaeryang Baek 541fa37c5c Merge pull request #8245 from open-webui/dependabot/pip/backend/dev/peewee-3.17.8
build(deps): bump peewee from 3.17.6 to 3.17.8 in /backend
2024-12-31 20:00:01 -08:00
Timothy Jaeryang Baek 06301aa90d Merge pull request #8243 from open-webui/dependabot/pip/backend/dev/validators-0.34.0
build(deps): bump validators from 0.33.0 to 0.34.0 in /backend
2024-12-31 19:59:45 -08:00
Timothy Jaeryang Baek 55fbc60ad3 Merge pull request #8244 from open-webui/dependabot/pip/backend/dev/flask-3.1.0
build(deps): bump flask from 3.0.3 to 3.1.0 in /backend
2024-12-31 19:59:36 -08:00
Timothy Jaeryang Baek b69941caf4 Merge pull request #8242 from open-webui/dependabot/pip/backend/dev/fpdf2-2.8.2
build(deps): bump fpdf2 from 2.7.9 to 2.8.2 in /backend
2024-12-31 19:59:13 -08:00
Timothy Jaeryang Baek 80600dbe63 Merge pull request #8241 from open-webui/dependabot/pip/backend/dev/pyjwt-crypto--2.10.1
build(deps): bump pyjwt[crypto] from 2.9.0 to 2.10.1 in /backend
2024-12-31 19:58:59 -08:00
Timothy Jaeryang Baek 085f48366b refac 2024-12-31 19:54:11 -08:00
Timothy Jaeryang Baek 60fc24cd39 refac: channel message input 2024-12-31 19:42:49 -08:00
dependabot[bot] 7fb87ddf99 build(deps): bump peewee from 3.17.6 to 3.17.8 in /backend
Bumps [peewee](https://github.com/coleifer/peewee) from 3.17.6 to 3.17.8.
- [Release notes](https://github.com/coleifer/peewee/releases)
- [Changelog](https://github.com/coleifer/peewee/blob/master/CHANGELOG.md)
- [Commits](https://github.com/coleifer/peewee/compare/3.17.6...3.17.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 02:15:02 +00:00
dependabot[bot] b41c1128c6 build(deps): bump flask from 3.0.3 to 3.1.0 in /backend
Bumps [flask](https://github.com/pallets/flask) from 3.0.3 to 3.1.0.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.0.3...3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 02:14:54 +00:00
dependabot[bot] 5bb308a098 build(deps): bump validators from 0.33.0 to 0.34.0 in /backend
Bumps [validators](https://github.com/python-validators/validators) from 0.33.0 to 0.34.0.
- [Release notes](https://github.com/python-validators/validators/releases)
- [Changelog](https://github.com/python-validators/validators/blob/master/CHANGES.md)
- [Commits](https://github.com/python-validators/validators/compare/0.33.0...0.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 02:14:51 +00:00
dependabot[bot] 236410cd95 build(deps): bump fpdf2 from 2.7.9 to 2.8.2 in /backend
Bumps [fpdf2](https://github.com/py-pdf/fpdf2) from 2.7.9 to 2.8.2.
- [Release notes](https://github.com/py-pdf/fpdf2/releases)
- [Changelog](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/fpdf2/compare/2.7.9...2.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 02:14:48 +00:00
dependabot[bot] f1800e2853 build(deps): bump pyjwt[crypto] from 2.9.0 to 2.10.1 in /backend
Bumps [pyjwt[crypto]](https://github.com/jpadilla/pyjwt) from 2.9.0 to 2.10.1.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.9.0...2.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 02:14:45 +00:00
Timothy Jaeryang Baek 4bc9904b3c Merge pull request #8159 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.5.3
2024-12-31 13:41:03 -08:00
Timothy Jaeryang Baek 995f80002f doc: changelog 2024-12-31 13:40:49 -08:00
Timothy Jaeryang Baek ffdb45cc4b refac 2024-12-31 13:39:03 -08:00
Timothy Jaeryang Baek f5ee5deec8 chore: format 2024-12-31 13:30:59 -08:00
Timothy Jaeryang Baek 02777ffc54 doc: changelog 2024-12-31 13:23:05 -08:00
Timothy Jaeryang Baek a5627d4226 refac 2024-12-31 13:09:17 -08:00
Timothy Jaeryang Baek 435a174316 Merge pull request #8232 from okamototk/improve-jp-trans
i18n - Updated Japanese Translation
2024-12-31 13:04:58 -08:00
Timothy Jaeryang Baek d905202afb refac 2024-12-31 13:04:27 -08:00
Takashi Okamoto 221cc19ac6 i18n - Updated Japanese Translation 2024-12-31 11:58:43 +00:00
Timothy Jaeryang Baek 3d69eaf34f refac: styling 2024-12-31 03:57:43 -08:00
Timothy Jaeryang Baek e7c501677c refac 2024-12-31 03:54:43 -08:00
Timothy Jaeryang Baek 96b1c2024b refac 2024-12-31 03:45:54 -08:00
Timothy Jaeryang Baek f7ecfafe1a refac: styling 2024-12-31 03:25:25 -08:00
Timothy Jaeryang Baek 6ecee8b920 refac: emoji picker optimisation 2024-12-31 03:24:05 -08:00
Timothy Jaeryang Baek cce8f37ada refac 2024-12-31 02:58:15 -08:00
Timothy Jaeryang Baek 60247fe18d refac 2024-12-31 02:51:52 -08:00
Timothy Jaeryang Baek de4e086bd5 refac 2024-12-31 02:48:43 -08:00
Timothy Jaeryang Baek 3da7ff1721 refac 2024-12-31 02:42:46 -08:00
Timothy Jaeryang Baek c8cdc6377b refac 2024-12-31 02:30:03 -08:00
Timothy Jaeryang Baek 85038cef69 refac: styling 2024-12-31 02:27:49 -08:00
Timothy Jaeryang Baek 016b91adda chore: format 2024-12-31 02:26:30 -08:00
Timothy Jaeryang Baek 0846a08795 Merge pull request #8230 from moblangeois/dev
i18n: update fr-FR
2024-12-31 02:24:42 -08:00
Timothy Jaeryang Baek 277eef62c2 Merge pull request #8210 from open-webui/channels
enh: channel threads & reactions
2024-12-31 02:24:10 -08:00
Timothy Jaeryang Baek 9f19933e64 refac: styling 2024-12-31 02:23:26 -08:00
Timothy Jaeryang Baek 904ddcec13 refac: styling 2024-12-31 02:19:22 -08:00
Timothy Jaeryang Baek 77da0a6cc1 refac: styling 2024-12-31 02:17:39 -08:00
Timothy Jaeryang Baek a754a4388a feat: channel threads 2024-12-31 02:16:07 -08:00
Morgan Blangeois 93f262c6a2 i18n: update fr-FR 2024-12-31 11:09:31 +01:00
Timothy Jaeryang Baek 16a48ef4eb fix 2024-12-31 02:06:55 -08:00
Timothy Jaeryang Baek 841f74894e refac 2024-12-31 02:06:19 -08:00
Timothy Jaeryang Baek 9c337552e6 enh: reply count 2024-12-31 02:05:11 -08:00
Timothy Jaeryang Baek 584e9e6da5 refac: threads 2024-12-31 00:51:43 -08:00
Timothy Jaeryang Baek a3dfa90668 refac: optimisation 2024-12-31 00:11:46 -08:00
Timothy Jaeryang Baek 00e8849445 refac 2024-12-30 23:55:19 -08:00
Timothy Jaeryang Baek 2840ff405b refac 2024-12-30 23:48:55 -08:00
Timothy Jaeryang Baek 9d39404e6c refac: styling 2024-12-30 23:16:50 -08:00
Timothy Jaeryang Baek 371201d820 refac 2024-12-30 23:12:50 -08:00
Timothy Jaeryang Baek f93c2e4a8d feat: reactions 2024-12-30 23:06:34 -08:00
Timothy Jaeryang Baek 4b0fa112bb Merge pull request #8222 from open-webui/dev
dev
2024-12-30 17:55:13 -08:00
Timothy Jaeryang Baek fd0170c179 revert 2024-12-30 16:55:29 -08:00
Timothy Jaeryang Baek 9b56b64cfa Merge pull request #8212 from ashm-dev/main
feat: Small optimization
2024-12-30 16:00:18 -08:00
Timothy Jaeryang Baek 887627fa34 Merge pull request #8217 from gabriel-ecegi/batch_fix
fix: Add missing request parameter to knowledge and retrieval routes
2024-12-30 15:58:10 -08:00
Timothy Jaeryang Baek 46bcf98ef2 fix: usage stats 2024-12-30 15:52:07 -08:00
Timothy Jaeryang Baek 947f5600d6 refac 2024-12-30 15:39:35 -08:00
Timothy Jaeryang Baek 79ce6e0a3f refac 2024-12-30 11:29:18 -08:00
Gabriel Ecegi 46e57706c1 refac: formatting 2024-12-30 17:45:43 +01:00
Gabriel Ecegi ba2964cb01 fix: missing parameter 2024-12-30 17:36:34 +01:00
shamil a0aee4ff28 feat: Small optimization 2024-12-30 13:45:20 +03:00
Timothy Jaeryang Baek 45289b0fa8 refac 2024-12-30 02:21:45 -08:00
Timothy Jaeryang Baek eece28ccc6 feat: emoji picker 2024-12-30 02:20:09 -08:00
Timothy Jaeryang Baek c216d89520 enh: threads & reactions 2024-12-29 23:16:18 -08:00
Timothy Jaeryang Baek d94134ad2d refac 2024-12-29 22:11:20 -08:00
Timothy Jaeryang Baek 952822ad5d refac 2024-12-29 21:02:11 -08:00
Timothy Jaeryang Baek 79d7e67e51 fix: ollama options 2024-12-29 16:30:36 -08:00
Timothy Jaeryang Baek 6de1923151 Merge pull request #8192 from TiancongLx/dev
i18n: update zh-TW
2024-12-29 15:36:19 -08:00
Tiancong Li abdbd6a67e i18n: update zh-TW 2024-12-30 03:13:20 +08:00
Timothy Jaeryang Baek a14dee87e8 Merge pull request #8184 from vishwamartur/fix-offline-docker
Fix offline docker container startup issue
2024-12-28 22:26:38 -08:00
Vishwanath Martur 00e6ffe83c Fix offline docker container startup issue
Related to #7207

Modify the code to allow the docker container to start in an offline environment for versions >= 0.4.0.

* **backend/open_webui/retrieval/utils.py**
  - Import `OFFLINE_MODE` from `open_webui.env`.
  - Set `local_files_only` to `True` when `OFFLINE_MODE` is enabled in `snapshot_kwargs`.

* **backend/open_webui/env.py**
  - Add logic to set `HF_HUB_OFFLINE` environment variable to `1` when `OFFLINE_MODE` is enabled.

* **README.md**
  - Document setting `HF_HUB_OFFLINE` environment variable to `1` for offline environments.
2024-12-29 11:53:09 +05:30
Timothy Jaeryang Baek 36d927e1ba refac 2024-12-28 19:31:03 -08:00
Timothy Jaeryang Baek 091e9e2f40 refac 2024-12-28 18:08:30 -08:00
Timothy Jaeryang Baek 54daf3b765 fix: outlet filter hook 2024-12-28 18:05:07 -08:00
Timothy Jaeryang Baek 76decdba2a fix: delete file 2024-12-28 17:40:00 -08:00
Timothy Jaeryang Baek e6e8978f78 refac 2024-12-28 16:42:43 -08:00
Timothy Jaeryang Baek fcaeb2f539 Merge pull request #8179 from cvaz1306/add_reset_button_mermaidjs_renderer
i18n: Add reset panzoom button to SVGPanZoom and cleanup
2024-12-28 16:27:44 -08:00
cvaz1306 f7d1833f41 Fixing formatting 2024-12-28 15:50:23 -08:00
cvaz1306 95598e5435 Merge branch 'dev' into add_reset_button_mermaidjs_renderer 2024-12-28 15:46:23 -08:00
cvaz1306 8a7e8b57a8 Add reset panzoom button to SVGPanZoom and cleanup 2024-12-28 15:27:47 -08:00
Timothy Jaeryang Baek 5a720a4a31 refac 2024-12-27 22:46:50 -08:00
Timothy Jaeryang Baek d55884b50e enh: ENABLE_REALTIME_CHAT_SAVE 2024-12-27 22:36:14 -08:00
Timothy Jaeryang Baek c8fb11db59 Merge pull request #8158 from cvaz1306/add_reset_button_mermaidjs_renderer
feat: Add reset button mermaidjs renderer
2024-12-27 17:55:00 -08:00
cvaz1306 7b3a86475d Add reset panzoom button to SVGPanZoom and cleanup 2024-12-27 17:06:15 -08:00
Timothy Jaeryang Baek 59ed53ec1e Merge pull request #8145 from bernadinm/fix/modified_update_ollama_models.sh
fix: Add execute permissions to update_ollama_models.sh
2024-12-27 16:24:32 -08:00
Timothy Jaeryang Baek 7f8b6ebbd5 refac: styling 2024-12-27 12:24:58 -08:00
Miguel Bernadin 3f0dd80898 fix: chmod +x update_ollama_models.sh 2024-12-27 08:29:54 -08:00
Timothy Jaeryang Baek 2299f48430 Merge pull request #8131 from open-webui/dev
fix: api key restrictions
2024-12-27 00:32:37 -08:00
Timothy Jaeryang Baek 99386bf680 fix: api key restrictions 2024-12-27 00:32:25 -08:00
Timothy Jaeryang Baek 4e4e2792b1 Merge pull request #8130 from open-webui/dev
refac
2024-12-27 00:06:41 -08:00
Timothy Jaeryang Baek d99e3d49e1 refac 2024-12-27 00:05:32 -08:00
Timothy Jaeryang Baek e42cbf07f5 Merge pull request #8122 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.5.2
2024-12-26 23:51:49 -08:00
Timothy Jaeryang Baek 016862a6b8 doc: changelog 2024-12-26 23:51:40 -08:00
Timothy Jaeryang Baek cd9dae0f3a refac 2024-12-26 23:51:30 -08:00
Timothy Jaeryang Baek b7baf4ddef doc: changelog 2024-12-26 23:46:07 -08:00
Timothy Jaeryang Baek 1584015d3b chore: format 2024-12-26 23:35:56 -08:00
Timothy Jaeryang Baek e4b4a78e52 refac 2024-12-26 23:35:01 -08:00
Timothy Jaeryang Baek 50534a0dcf enh: user status indicator 2024-12-26 23:29:33 -08:00
Timothy Jaeryang Baek c53ace3c98 refac 2024-12-26 22:03:39 -08:00
Timothy Jaeryang Baek 7fa60dd63e refac 2024-12-26 21:55:21 -08:00
Timothy Jaeryang Baek 6ff6d57507 enh: typing indicator 2024-12-26 21:51:09 -08:00
Timothy Jaeryang Baek 4f93ecf519 refac 2024-12-26 20:58:46 -08:00
Timothy Jaeryang Baek 1e974439d9 enh: configurable api key endpoint restrictions 2024-12-26 20:57:51 -08:00
Timothy Jaeryang Baek 611955bc91 refac 2024-12-26 20:38:27 -08:00
Timothy Jaeryang Baek e3937ada38 fix: ollama usage 2024-12-26 20:35:14 -08:00
Timothy Jaeryang Baek c5b8466c0e fix: playground 2024-12-26 19:12:08 -08:00
Timothy Jaeryang Baek e25b082162 fix: pipelines outlet 2024-12-26 19:10:28 -08:00
Timothy Jaeryang Baek 247d7896fc Merge pull request #8093 from aleixdorca/dev
i18n: Updated Catalan translation.json
2024-12-26 13:34:30 -08:00
Timothy Jaeryang Baek a0ad681c77 Merge pull request #8101 from panda44312/dev
i18n - Updated Simplified Chinese Translation
2024-12-26 13:34:21 -08:00
Timothy Jaeryang Baek 1b7ee5dd2b Merge pull request #8115 from taylorwilsdon/fix_drive_integration
Fix drive integration
2024-12-26 13:34:09 -08:00
Timothy Jaeryang Baek 8b6d03e430 fix: elevenlabs audio 2024-12-26 12:54:31 -08:00
Taylor Wilsdon efc4ad9f65 Update Documents.svelte 2024-12-26 14:48:38 -05:00
Taylor Wilsdon 3cfb6507c5 Update .gitignore 2024-12-26 14:47:41 -05:00
Taylor Wilsdon 24a314876b formatting 2024-12-26 14:46:41 -05:00
Taylor Wilsdon 190911206b Remove logging 2024-12-26 14:46:06 -05:00
Taylor Wilsdon ea11521961 Remove unnecessary logging 2024-12-26 14:44:36 -05:00
Taylor Wilsdon 909c94e983 Fix Google Drive integration 2024-12-26 14:41:58 -05:00
Taylor Wilsdon (aider) 1c7ff7b1dc fix: Initialize enableGoogleDriveIntegration with default value 2024-12-26 12:22:58 -05:00
Taylor Wilsdon (aider) c7cec2131c debug: Add detailed logging for Google Drive Integration state changes 2024-12-26 12:21:38 -05:00
Taylor Wilsdon (aider) 6f3e7ed0e9 debug: Add console logs for Google Drive Integration state changes 2024-12-26 12:19:53 -05:00
Panda d4ccd25760 Update translation.json 2024-12-26 11:51:44 +01:00
Timothy Jaeryang Baek dd933e406f Merge pull request #8095 from open-webui/dev
fix: automatic1111 image generation
2024-12-25 23:35:10 -08:00
Timothy Jaeryang Baek 3c74ce3653 fix: automatic1111 image generation 2024-12-25 23:34:54 -08:00
Timothy Jaeryang Baek de2bda3e2e Merge pull request #8094 from open-webui/dev
refac: title generation logic
2024-12-25 23:52:21 -07:00
Timothy Jaeryang Baek 4b3e1bb747 refac: title generation logic 2024-12-25 22:51:08 -08:00
Aleix Dorca bb30a83520 Update catalan translation.json 2024-12-26 07:49:49 +01:00
Timothy Jaeryang Baek 2bdf99b398 Merge pull request #8071 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.5.1
2024-12-25 23:31:01 -07:00
Timothy Jaeryang Baek 96a06effe2 doc: wording 2024-12-25 22:30:52 -08:00
Timothy Jaeryang Baek df4a8e167d doc: changelog 2024-12-25 22:28:20 -08:00
Timothy Jaeryang Baek c16896e96d refac 2024-12-25 22:23:59 -08:00
Timothy Jaeryang Baek da7fa09053 fix: non-stream chat completion 2024-12-25 22:21:44 -08:00
Timothy Jaeryang Baek 23bf71022e fix: title generation issue 2024-12-25 16:46:49 -07:00
Timothy Jaeryang Baek 798886105e enh: option to toggle notification sound 2024-12-25 13:49:24 -07:00
Timothy Jaeryang Baek 645313e321 Merge pull request #8062 from OriginalSimon/main
18n: Update Ukrainian translation
2024-12-25 12:44:32 -08:00
Timothy Jaeryang Baek 0915352cb0 Merge pull request #8068 from open-webui/dev
fix: get users
2024-12-25 12:39:27 -08:00
Timothy Jaeryang Baek 70a108e54f fix: get users 2024-12-25 13:39:04 -07:00
Timothy Jaeryang Baek a8289745da Merge pull request #8066 from open-webui/dev
fix: get_automatic1111_api_auth
2024-12-25 12:26:59 -08:00
Timothy Jaeryang Baek d3ba77837a fix: get_automatic1111_api_auth 2024-12-25 13:26:13 -07:00
Timothy Jaeryang Baek 3161a5d1f7 Merge pull request #8063 from open-webui/dev
fix: pipelines not loading
2024-12-25 12:08:27 -08:00
Timothy Jaeryang Baek 3a35f9ae7c fix: pipelines not loading 2024-12-25 13:07:55 -07:00
Simon 12cf2b0104 Update translation.json 2024-12-25 20:34:01 +01:00
Timothy Jaeryang Baek 22132e155a Merge pull request #7896 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.5.0
2024-12-25 10:39:01 -08:00
Timothy Jaeryang Baek d7d08b40ed fix: pip install not working 2024-12-25 11:36:40 -07:00
Timothy Jaeryang Baek 4f886c3944 doc: wording 2024-12-25 10:52:40 -07:00
Timothy Jaeryang Baek afdb162f4f doc: changelog 2024-12-25 10:45:45 -07:00
Timothy Jaeryang Baek a4b5a9ac09 chore: format 2024-12-25 09:51:23 -07:00
Timothy Jaeryang Baek c4937cc144 enh: webui url 2024-12-25 09:50:57 -07:00
Timothy Jaeryang Baek b5bb853c66 chore: format 2024-12-25 09:38:51 -07:00
Timothy Jaeryang Baek cd367534b7 refac 2024-12-25 02:32:47 -07:00
Timothy Jaeryang Baek 2e5c2bc4c2 refac 2024-12-25 00:57:39 -07:00
Timothy Jaeryang Baek 34cc472c48 refac 2024-12-25 00:53:50 -07:00
Timothy Jaeryang Baek d701b69e05 enh: channel notification 2024-12-25 00:53:25 -07:00
Timothy Jaeryang Baek 0d7d6899b9 refac 2024-12-24 23:45:21 -07:00
Timothy Jaeryang Baek a2366a20ba refac: api key auth allowed paths 2024-12-24 23:32:34 -07:00
Timothy Jaeryang Baek 326514be4e enh: image compression 2024-12-24 23:28:14 -07:00
Timothy Jaeryang Baek 591aac5e16 chore: format 2024-12-24 22:44:21 -07:00
Timothy Jaeryang Baek e10897236d chore: format 2024-12-24 22:44:10 -07:00
Timothy Jaeryang Baek 91429640ff Merge pull request #8053 from Adhithya03/main
enh: add configurable log level for uvicorn server
2024-12-24 21:42:03 -08:00
Timothy Jaeryang Baek d0828711bd refac: styling 2024-12-24 22:36:57 -07:00
Timothy Jaeryang Baek 11b36fe03e refac 2024-12-24 22:33:42 -07:00
Timothy Jaeryang Baek 47419a77af refac 2024-12-24 22:31:03 -07:00
Timothy Jaeryang Baek d93107d1d6 refac 2024-12-24 22:16:22 -07:00
Timothy Jaeryang Baek e39617b1c0 refac 2024-12-24 22:04:43 -07:00
Timothy Jaeryang Baek 31a97d8fec refac: styling 2024-12-24 21:30:30 -07:00
Timothy Jaeryang Baek f91e56d6df refac 2024-12-24 21:28:35 -07:00
Timothy Jaeryang Baek 688f11e1c5 refac 2024-12-24 21:17:24 -07:00
Timothy Jaeryang Baek 4442411f40 refac 2024-12-24 21:16:09 -07:00
Timothy Jaeryang Baek cd7eff3bdb refac 2024-12-24 20:37:59 -07:00
Timothy Jaeryang Baek 0d70ae6307 refac: message input styling 2024-12-24 20:20:38 -07:00
Timothy Jaeryang Baek e61943f55f refac: styling 2024-12-24 20:16:02 -07:00
Timothy Jaeryang Baek f8269de947 fix 2024-12-24 20:10:52 -07:00
Timothy Jaeryang Baek 4b7f0c5be1 refac 2024-12-24 19:39:53 -07:00
Timothy Jaeryang Baek cd86161f33 refac 2024-12-24 19:34:56 -07:00
Timothy Jaeryang Baek e51722348a refac 2024-12-24 19:27:17 -07:00
Timothy Jaeryang Baek 346856b578 refac 2024-12-24 18:41:06 -07:00
Timothy Jaeryang Baek b70a31f81e enh: notification sound 2024-12-24 18:33:49 -07:00
Timothy Jaeryang Baek 2d44cd4cda enh: chat/channel notification toast 2024-12-24 18:25:59 -07:00
Timothy Jaeryang Baek 46e319dedc refac 2024-12-24 18:04:41 -07:00
Timothy Jaeryang Baek 55da6224b8 enh: save status from the backend 2024-12-24 18:03:14 -07:00
Timothy Jaeryang Baek 95da0734b6 refac 2024-12-24 17:56:46 -07:00
Timothy Jaeryang Baek 6b25139d4f refac: web search 2024-12-24 17:52:57 -07:00
Timothy Jaeryang Baek a074991d3a refac 2024-12-24 17:01:17 -07:00
Timothy Jaeryang Baek 00f3a9cb52 refac 2024-12-24 16:56:52 -07:00
Timothy Jaeryang Baek a2e0fbc943 refac: collection query status 2024-12-24 16:49:32 -07:00
Timothy Jaeryang Baek 01649fad64 enh: esc to stop response 2024-12-24 15:38:54 -07:00
Timothy Jaeryang Baek e1a198f0a3 fix: prompt import 2024-12-24 15:32:23 -07:00
Timothy Jaeryang Baek 364d6eb9c4 Merge pull request #8042 from panda44312/dev
i18n - Updated Simplified Chinese Translation
2024-12-24 14:28:41 -08:00
Panda 8511847320 Update translation.json 2024-12-24 18:19:15 +01:00
Adhithya d52fc40038 format 2024-12-24 22:25:13 +05:30
Adhithya03 16da847342 enh: add configurable log level for uvicorn server 2024-12-24 22:18:31 +05:30
Timothy Jaeryang Baek ecd3b4ebd4 enh: channel file upload 2024-12-23 14:43:58 -07:00
Timothy Jaeryang Baek b4d7268bed fix 2024-12-23 14:00:58 -07:00
Timothy Jaeryang Baek 689b910c77 refac 2024-12-23 01:38:45 -07:00
Timothy Jaeryang Baek 582253fc68 refac 2024-12-23 01:37:13 -07:00
Timothy Jaeryang Baek 6d02485999 refac: styling 2024-12-23 01:35:03 -07:00
Timothy Jaeryang Baek 190aeb3fef refac 2024-12-23 01:29:59 -07:00
Timothy Jaeryang Baek 51e0ed454c chore: format 2024-12-23 01:25:25 -07:00
Timothy Jaeryang Baek f05dbb895e Merge pull request #8015 from open-webui/channels
feat: channels
2024-12-23 00:23:05 -08:00
Timothy Jaeryang Baek 83d2bf1c0d refac: styling 2024-12-23 01:22:43 -07:00
Timothy Jaeryang Baek 5cca378cc8 refac 2024-12-23 01:22:04 -07:00
Timothy Jaeryang Baek ed44b21a78 enh: edited indicator 2024-12-23 01:19:30 -07:00
Timothy Jaeryang Baek fb3c297df2 enh: message edit/delete integration 2024-12-23 01:12:55 -07:00
Timothy Jaeryang Baek 2c8fb66383 refac 2024-12-23 01:06:59 -07:00
Timothy Jaeryang Baek b44b7e8162 refac 2024-12-23 01:03:59 -07:00
Timothy Jaeryang Baek 15fa7b44ea refac: styling 2024-12-23 01:03:14 -07:00
Timothy Jaeryang Baek 83099a093d enh: message edit 2024-12-23 00:53:45 -07:00
Timothy Jaeryang Baek cdc75237b2 refac: styling 2024-12-23 00:41:22 -07:00
Timothy Jaeryang Baek 76c8602324 refac 2024-12-23 00:31:33 -07:00
Timothy Jaeryang Baek 4c756b5501 enh: channel delete 2024-12-22 23:15:29 -07:00
Timothy Jaeryang Baek 7ad8918cd9 enh: update channel 2024-12-22 23:09:51 -07:00
Timothy Jaeryang Baek e9194d9524 refac 2024-12-22 22:39:44 -07:00
Timothy Jaeryang Baek e93b37bab1 refac: styling 2024-12-22 22:37:14 -07:00
Timothy Jaeryang Baek 74cacf8bf5 enh: channel navbar 2024-12-22 22:33:13 -07:00
Timothy Jaeryang Baek 198bd49cc2 enh: channel messages 2024-12-22 22:24:09 -07:00
Timothy Jaeryang Baek a4333295ce refac 2024-12-22 22:20:24 -07:00
Timothy Jaeryang Baek 5748f6ef77 refac 2024-12-22 22:08:27 -07:00
Timothy Jaeryang Baek c6dcac99ac refac: db schema 2024-12-22 22:06:16 -07:00
Timothy Jaeryang Baek 698f2f4e51 refac 2024-12-22 21:57:32 -07:00
Timothy Jaeryang Baek a165e76486 refac: styling 2024-12-22 21:56:51 -07:00
Timothy Jaeryang Baek f4e5e5171f feat: channels backend 2024-12-22 21:50:14 -07:00
Timothy Jaeryang Baek cb3e01de8a enh: channels enable/disable option 2024-12-22 21:02:14 -07:00
Timothy Jaeryang Baek 0d29f31846 refacx 2024-12-22 20:28:15 -07:00
Timothy Jaeryang Baek 5e8f3048f9 refac 2024-12-22 19:47:40 -07:00
Timothy Jaeryang Baek f1d21fc59a feat: channel socket integration 2024-12-22 19:40:01 -07:00
Timothy Jaeryang Baek eaecd15e69 refac 2024-12-22 17:16:14 -07:00
Timothy Jaeryang Baek 2914c29ab3 refac: styling 2024-12-22 15:11:10 -07:00
Timothy Jaeryang Baek e444f769f6 refac 2024-12-22 04:49:24 -07:00
Timothy Jaeryang Baek 2e85c8e24d refac 2024-12-22 04:10:10 -07:00
Timothy Jaeryang Baek 7c8de9e221 feat: channels backend 2024-12-22 03:42:19 -07:00
Timothy Jaeryang Baek 79aae7a76e refac: styling 2024-12-22 02:47:32 -07:00
Timothy Jaeryang Baek 7083a61ddb refac 2024-12-21 18:25:16 -07:00
Timothy Jaeryang Baek 27d2fbbe33 refac: sidebar styling 2024-12-21 18:12:44 -07:00
Timothy Jaeryang Baek 24c3f7a664 fix: custom model 2024-12-21 16:29:48 -07:00
Timothy Jaeryang Baek 06a692282b refac 2024-12-21 16:08:20 -07:00
Timothy Jaeryang Baek 556c75e876 refac 2024-12-21 09:59:12 -07:00
Timothy Jaeryang Baek 271acb2e67 refac 2024-12-21 09:45:52 -07:00
Timothy Jaeryang Baek c611734088 refac: styling 2024-12-21 09:41:49 -07:00
Timothy Jaeryang Baek 41cabf5a2c chore: format 2024-12-21 09:20:20 -07:00
Timothy Jaeryang Baek 6981e1e467 enh: preserve input 2024-12-21 09:16:29 -07:00
Timothy Jaeryang Baek 7a787efc4b Merge pull request #7988 from TiancongLx/dev
i18n: update zh-TW
2024-12-21 07:51:24 -08:00
Tiancong Li 9d3ab2e40e Merge branch 'open-webui:dev' into dev 2024-12-21 15:48:08 +08:00
Tiancong Li 2b7213f04a i18n: update zh-TW 2024-12-21 15:47:02 +08:00
Timothy Jaeryang Baek de2825bb89 refac 2024-12-20 23:09:40 -08:00
Timothy Jaeryang Baek 423fee347a refac: discord webhook 2024-12-20 23:05:22 -08:00
Timothy Jaeryang Baek 2fd7bbc259 refac 2024-12-20 22:56:37 -08:00
Timothy Jaeryang Baek 50db2514dc refac 2024-12-20 22:55:46 -08:00
Timothy Jaeryang Baek 4820ecc371 enh: webhook notification 2024-12-20 22:54:43 -08:00
Timothy Jaeryang Baek 03cfac185f chore: bump 2024-12-20 19:57:26 -08:00
Timothy Jaeryang Baek da0438f08c fix 2024-12-20 19:13:17 -08:00
Timothy Jaeryang Baek cf0aca1487 fix 2024-12-20 18:37:25 -08:00
Timothy Jaeryang Baek b1b2a6d78c refac 2024-12-20 18:31:20 -08:00
Timothy Jaeryang Baek e9cb6e4714 fix 2024-12-20 18:29:57 -08:00
Timothy Jaeryang Baek 8cc834901f refac 2024-12-20 15:21:27 -08:00
Timothy Jaeryang Baek 6524cae407 refac 2024-12-20 15:19:54 -08:00
Timothy Jaeryang Baek 47318daef0 enh: add to conversation 2024-12-20 15:09:17 -08:00
Timothy Jaeryang Baek ad5cc9d79e refac 2024-12-20 14:44:12 -08:00
Timothy Jaeryang Baek 37ce88e744 refac: floating buttons 2024-12-20 14:38:15 -08:00
Timothy Jaeryang Baek d6f0c77c34 Merge pull request #7978 from OriginalSimon/dev
18n: Update Ukrainian translation
2024-12-20 14:08:11 -08:00
Simon 544f516104 Update translation.json 2024-12-20 17:28:04 +01:00
Timothy Jaeryang Baek 2721c5939e Merge pull request #7967 from aleixdorca/dev
Update catalan translation.json
2024-12-20 00:22:25 -08:00
Aleix Dorca 2cd75ceb53 Update catalan translation.json 2024-12-20 09:10:57 +01:00
Timothy Jaeryang Baek bf4f71879f refac 2024-12-19 20:59:45 -08:00
Timothy Jaeryang Baek 50f36a5262 refac: styling 2024-12-19 20:56:16 -08:00
Timothy Jaeryang Baek ef5a5be60d fix 2024-12-19 20:16:24 -08:00
Timothy Jaeryang Baek eabd9192f3 refac 2024-12-19 20:11:13 -08:00
Timothy Jaeryang Baek 38208866b9 fix 2024-12-19 20:01:18 -08:00
Timothy Jaeryang Baek db9aef0eaf refac 2024-12-19 19:05:20 -08:00
Timothy Jaeryang Baek 70de5cf7b8 fix: audio 2024-12-19 16:18:54 -08:00
Timothy Jaeryang Baek 455b38dcc1 Fix code scanning alert no. 150: Information exposure through an exception
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-12-19 15:59:03 -08:00
Timothy Jaeryang Baek 49b36937ad refac 2024-12-19 15:45:31 -08:00
Timothy Jaeryang Baek 8535974ea3 refac: styling 2024-12-19 15:16:51 -08:00
Timothy Jaeryang Baek d9573befff feat: chat completion notification 2024-12-19 15:14:09 -08:00
Timothy Jaeryang Baek f133353734 Merge pull request #7964 from jk-f5/fix/cleanup
Fix Redis Lock Expiration in periodic_usage_pool_cleanup
2024-12-19 14:45:18 -08:00
Timothy Jaeryang Baek 6b713bc487 Merge pull request #7963 from panda44312/dev
i18n: Updated Simplified Chinese translation, hopefully won't miss version 5.0.0!
2024-12-19 14:20:23 -08:00
Panda 7268ea7abd Update translation.json 2024-12-19 23:17:46 +01:00
Jason Kidd 03e48de1a9 fix: Issue in some environments running in dev mode with redis where periodic cleanup takes longer than TIMEOUT_DURATION*2 seconds to be called and the lock expires. 2024-12-19 14:15:02 -08:00
Timothy Jaeryang Baek 8455396249 fix: websocket redis 2024-12-19 13:46:30 -08:00
Timothy Jaeryang Baek 16504b88f5 fix 2024-12-19 13:11:44 -08:00
Timothy Jaeryang Baek 71d4ed1f6a Merge pull request #7955 from matthewdtwo/dev
fix: Hide workspace model community share button when community sharing is disabled.
2024-12-19 12:45:07 -08:00
Timothy Jaeryang Baek 0db0b8ce2c fix 2024-12-19 12:19:06 -08:00
Timothy Jaeryang Baek 1ea00a58f9 refac 2024-12-19 12:16:47 -08:00
Timothy Jaeryang Baek 4c989808d6 refac 2024-12-19 11:07:02 -08:00
Matthew 0b9c6466ce Hide community share button when community sharing is disabled.
Only show the share button on the workspaces model menu when Community sharing is enabled.
2024-12-19 10:48:02 -06:00
Timothy Jaeryang Baek c3e8cd03b2 chore: format 2024-12-19 01:07:50 -08:00
Timothy Jaeryang Baek 0d5ce23885 refac 2024-12-19 01:05:47 -08:00
Timothy Jaeryang Baek 64fe2de962 refac 2024-12-19 01:02:05 -08:00
Timothy Jaeryang Baek 2be9e55545 refac: chat requests 2024-12-19 01:00:32 -08:00
Timothy Jaeryang Baek ea0d507e23 chore: format 2024-12-18 18:33:41 -08:00
Timothy Jaeryang Baek 0523ebcc5e Merge pull request #7887 from jk-f5/disablepolling
Disable Polling Transport When WebSockets Are Enabled and Implement Cleanup Locking Mechanism
2024-12-18 18:32:56 -08:00
Timothy Jaeryang Baek e4573d0b6c refac 2024-12-18 18:32:19 -08:00
Timothy Jaeryang Baek ddac34f769 refac 2024-12-18 18:15:58 -08:00
Timothy Jaeryang Baek 2875326015 fix: table export 2024-12-18 18:11:01 -08:00
Timothy Jaeryang Baek 0f6d302760 refac 2024-12-18 18:04:56 -08:00
Timothy Jaeryang Baek 5871df02ac Merge pull request #7900 from taylorwilsdon/add_google_drive_integration
feat: Add Google Drive integration for Open-Webui
2024-12-18 18:00:13 -08:00
Taylor Wilsdon f3454a8bba Add google drive requirements to requirements.txt 2024-12-18 13:35:37 -05:00
Taylor Wilsdon 1120f4d09a npm run format 2024-12-18 13:32:46 -05:00
Taylor Wilsdon 0dc75363aa Add configurable Google Drive toggle in the Documents admin section along with necessary config scaffolding 2024-12-18 13:25:57 -05:00
Taylor Wilsdon (aider) 5c149c3aa2 style: Align Google Drive switch to the right side of text 2024-12-18 13:24:13 -05:00
Taylor Wilsdon d43ca803ca feat: Add Google Drive integration toggle to document settings 2024-12-18 13:24:11 -05:00
Taylor Wilsdon 366158ff04 npm run format 2024-12-18 12:18:31 -05:00
Taylor Wilsdon 89e86f5e2e functional 2024-12-18 12:15:23 -05:00
Taylor Wilsdon 76ca3cf452 upstream 2024-12-18 12:11:17 -05:00
Taylor Wilsdon e28427803f Fix dev upstream merge conflicts 2024-12-18 12:04:55 -05:00
Jason Kidd 8f51681801 feat: Make ENABLE_WEBSOCKET_SUPPORT disable polling entirely to allow multiple replicas without sticky sessions.
See https://socket.io/docs/v4/using-multiple-nodes/ for details why this was done.

Also create a redis key to track which replica is running the cleanup job
2024-12-18 07:54:12 -08:00
Timothy Jaeryang Baek a38934bd23 feat: screen capture 2024-12-18 01:27:32 -08:00
Timothy Jaeryang Baek e500461dc0 refac 2024-12-17 18:40:50 -08:00
Timothy Jaeryang Baek c7e3692678 Merge pull request #7919 from denispol/main
fix: enhance Markdown text cleaning for TTS compatibility
2024-12-17 13:54:58 -08:00
Timothy Jaeryang Baek 9abae36264 Merge pull request #7881 from gabriel-ecegi/dev
feat: Batch Processing for Large-Scale Document Import
2024-12-17 13:54:00 -08:00
Timothy Jaeryang Baek 5bdb1c99bb refac 2024-12-17 13:52:57 -08:00
Timothy Jaeryang Baek 1902d4238b chore: format 2024-12-17 13:51:29 -08:00
Timothy Jaeryang Baek 08398e511e Merge pull request #7905 from devdev999/make-swagger-available-offline
feat: make swagger docs available offline
2024-12-17 13:37:42 -08:00
Timothy Jaeryang Baek a14c06fa0b Merge pull request #7920 from envision3d/ollama-structured-output
feat: pass structured output format through to ollama
2024-12-17 13:37:24 -08:00
Timothy Jaeryang Baek 9531edf6d6 Merge pull request #7923 from tarmst/groups-from-oauth
feat: Allow user groups to be managed by oauth
2024-12-17 13:36:34 -08:00
Timothy Jaeryang Baek 1b599057e9 Merge pull request #7915 from short-circuit/dev
feat: add comfyui api key
2024-12-17 13:35:49 -08:00
tarmst 0f33856182 Removing prints used for debugging 2024-12-17 19:50:59 +00:00
tarmst 9737869d11 Adding oauth group management for users upon login 2024-12-17 19:38:07 +00:00
envision3d 16d900247a feat: pass structured output format through to ollama 2024-12-17 09:33:32 -06:00
denispol e6add2869b Update index.ts 2024-12-17 14:56:12 +01:00
denispol d87584e7ad refactor(utils): enhance Markdown text cleaning for TTS compatibility 2024-12-17 14:52:02 +01:00
Davide Del Popolo 987b0f0dfd feat: add comfyui api key 2024-12-17 08:29:00 +01:00
gabriel-ecegi 54f6ae8fb5 Merge branch 'open-webui:dev' into dev 2024-12-17 06:03:53 +01:00
KX d681357c0c fix: update swagger-ui version to latest release 5.18.2 2024-12-17 11:12:40 +08:00
Timothy Jaeryang Baek 18fd3db3d5 refac 2024-12-16 13:27:54 -08:00
Taylor Wilsdon 9000fddffc Merge branch 'add_google_drive_integration' of github.com:taylorwilsdon/open-webui into add_google_drive_integration 2024-12-16 15:33:19 -05:00
Taylor Wilsdon 7c9f04bd59 fix merge conflicts 2024-12-16 15:29:49 -05:00
Timothy Jaeryang Baek 6962f8f3b3 Fix code scanning alert no. 148: Information exposure through an exception
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-12-16 12:27:11 -08:00
Taylor Wilsdon a26d5a2d95 clean up drift 2024-12-16 15:24:55 -05:00
Taylor Wilsdon 61aed5fe28 clean up drift 2024-12-16 15:20:56 -05:00
Taylor Wilsdon a27df11c02 Fix formatting 2024-12-16 15:11:05 -05:00
Taylor Wilsdon bf9ff1af56 packagelock 2024-12-16 15:10:01 -05:00
Timothy Jaeryang Baek ad82f790c1 Fix code scanning alert no. 149: Information exposure through an exception
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-12-16 11:59:50 -08:00
Taylor Wilsdon (aider) f078bbf006 feat: Add Google Drive config values to /api/config endpoint 2024-12-16 14:58:14 -05:00
Taylor Wilsdon (aider) 513d88b26d feat: Add Google Drive config values to /api/config endpoint 2024-12-16 14:51:02 -05:00
Taylor Wilsdon (aider) edebbd2f6d refactor: Retrieve Google Drive credentials from main config endpoint 2024-12-16 14:48:19 -05:00
Taylor Wilsdon (aider) 49c1267089 fix: Update Google Drive credentials endpoint URL 2024-12-16 14:47:14 -05:00
Taylor Wilsdon (aider) 4528770a0e feat: Implement dynamic Google Drive credentials fetching from backend config 2024-12-16 14:44:17 -05:00
Taylor Wilsdon (aider) e4b2d29cba feat: Add Google Drive client ID and API key config options 2024-12-16 14:43:33 -05:00
Taylor Wilsdon (aider) 4215c8ac3f fix: Change Google Docs export format from markdown to plain text 2024-12-16 14:09:35 -05:00
Taylor Wilsdon (aider) b46f4ff3e0 feat: Change Google Docs export format to markdown 2024-12-16 13:49:53 -05:00
Taylor Wilsdon (aider) f4b9f77cc9 feat: Add support for Google Slides with text/plain export 2024-12-16 13:40:39 -05:00
Taylor Wilsdon (aider) 9fc4ef323c feat: Add CSV export support for Google Spreadsheets in Drive picker 2024-12-16 13:37:50 -05:00
Taylor Wilsdon 15f14d0318 fix formatting 2024-12-16 13:36:25 -05:00
KX ab8f8bcf92 feat: add offline swagger docs 2024-12-17 02:00:51 +08:00
Taylor Wilsdon (aider) 81bb816881 refactor: Remove spreadsheet export format handling in Google Drive picker 2024-12-16 11:11:21 -05:00
Taylor Wilsdon (aider) 8bf09ae29c feat: Remove spreadsheet support from Google Drive picker 2024-12-16 11:11:08 -05:00
Taylor Wilsdon (aider) 81e601002f fix: Handle collection_name for spreadsheet file uploads 2024-12-16 11:05:36 -05:00
Taylor Wilsdon (aider) 31f499af0e fix: Simplify Google Drive file upload handling for spreadsheets 2024-12-16 11:00:40 -05:00
Taylor Wilsdon (aider) 51061afb10 fix: Declare finalFileName to resolve Google Drive picker error 2024-12-16 10:58:47 -05:00
Taylor Wilsdon (aider) 886fb3f426 fix: Prevent constant reassignment in Google Drive file picker 2024-12-16 10:57:58 -05:00
Taylor Wilsdon (aider) f919f63d33 fix: Resolve constant reassignment warning in Google Drive picker filename handling 2024-12-16 10:57:13 -05:00
Taylor Wilsdon (aider) eae86f04c5 feat: Add Google Sheets support to file picker with .xlsx export 2024-12-16 10:54:12 -05:00
Taylor Wilsdon (aider) 0563239782 refactor: Remove verbose logging from file upload handlers 2024-12-16 10:47:18 -05:00
Taylor Wilsdon (aider) 7699db0666 refactor: Remove debug logs and streamline Google Drive picker code 2024-12-16 10:47:00 -05:00
Taylor Wilsdon (aider) 1cc4eb241a feat: Improve Google Drive file upload handling in chat context 2024-12-16 10:40:08 -05:00
Taylor Wilsdon (aider) ec26296dc3 fix: Improve Google Drive file download error handling and export formats 2024-12-16 10:32:50 -05:00
Taylor Wilsdon (aider) cc9b7a1f1a fix: Update Google Drive file download URL generation to handle different file types 2024-12-16 10:32:01 -05:00
Taylor Wilsdon (aider) ae589bf604 fix: Make Google Drive picker callback async to resolve await usage 2024-12-16 10:30:45 -05:00
Taylor Wilsdon (aider) 77490e3392 feat: Implement file download with blob in Google Drive picker 2024-12-16 10:29:35 -05:00
MadsLang 6f1065b56a Merge branch 'dev' of github.com:open-webui/open-webui 2024-12-16 10:00:24 +01:00
Timothy Jaeryang Baek d5152e43d5 refac 2024-12-15 23:52:25 -08:00
Timothy Jaeryang Baek f341971eae fix 2024-12-15 23:41:17 -08:00
Timothy Jaeryang Baek 35cdd43a31 fix: unicode file name 2024-12-15 23:08:51 -08:00
Timothy Jaeryang Baek 4a7c1d8d55 Merge pull request #7875 from MooreDerek/Fix-File-Upload-Logging
Chore: Tika file upload - only log file contents in debug
2024-12-15 22:47:29 -08:00
Timothy Jaeryang Baek ddfed87b15 fix 2024-12-15 22:44:47 -08:00
Gabriel Ecegi 46f2f0fbdb Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-12-16 07:34:06 +01:00
MooreDerek 4905c180a5 Only log file contents in debug 2024-12-16 15:58:26 +13:00
Taylor Wilsdon (aider) 1feaee8eca feat: Update Google Drive download URL format for better compatibility 2024-12-15 20:29:12 -05:00
Taylor Wilsdon (aider) 1dce50df12 refactor: Update Google Drive file upload logging with dynamic token 2024-12-15 20:09:44 -05:00
Taylor Wilsdon (aider) d1ca6922d9 fix: Replace hardcoded bearer token with dynamic token variable 2024-12-15 20:09:35 -05:00
Taylor Wilsdon (aider) 0e60ba4723 feat: Improve Google Drive file upload error handling and validation 2024-12-15 20:08:52 -05:00
Timothy Jaeryang Baek ff8e94e41f Merge pull request #7811 from kostich/main
i18n: Update Serbian translation
2024-12-15 17:08:30 -08:00
Timothy Jaeryang Baek 1d1c4bc85d Merge pull request #7553 from OhMyMndy/codemirror-languages
feat: add HCL highlighting
2024-12-15 17:06:22 -08:00
Timothy Jaeryang Baek bd88aa2af9 Merge pull request #7828 from juananpe/fixdownloadcitations
fix: use WEBUI_API_BASE_URL for file download links in Citations
2024-12-15 17:05:03 -08:00
Timothy Jaeryang Baek dd8f13e097 Merge pull request #7866 from HUD4K/cs-CZ-translation
i18n Czech and Slovak translation Added Slovak translation, improved Czech translation
2024-12-15 17:04:33 -08:00
Timothy Jaeryang Baek 495ecc531b Merge pull request #7850 from i0ntempest/new-python
Chore: Allow installing with python 3.12
2024-12-15 17:04:00 -08:00
Timothy Jaeryang Baek 72ab29013d Merge pull request #7772 from oyve/dev
i18n: Updates to Norwegian language
2024-12-15 17:03:41 -08:00
Timothy Jaeryang Baek 2d368e89a1 Merge pull request #7741 from sanch7/offline_mode_fix
[Fix] Turn off auto update in offline mode
2024-12-15 17:03:26 -08:00
Timothy Jaeryang Baek 3eea90b025 Merge pull request #7745 from open-webui/refac
general refac
2024-12-15 17:02:24 -08:00
Taylor Wilsdon (aider) e802004dc3 fix: Improve Google Drive file upload handling in Chat component 2024-12-15 19:58:49 -05:00
Taylor Wilsdon (aider) 4718239353 fix: Add comprehensive logging and error handling in Google Drive picker callback 2024-12-15 19:55:36 -05:00
Taylor Wilsdon (aider) a865420cb1 feat: Add detailed logging for file download process to diagnose download issues 2024-12-15 19:54:13 -05:00
Taylor Wilsdon (aider) 42af98ae28 feat: Add local file download for debugging Google Drive file upload 2024-12-15 19:52:39 -05:00
Taylor Wilsdon (aider) f4f8334153 fix: Use correct OAuth token and headers in Google Drive file upload 2024-12-15 19:50:51 -05:00
Taylor Wilsdon (aider) 1542cb486d fix: Improve Google Drive file upload handling and error logging 2024-12-15 19:49:26 -05:00
Taylor Wilsdon (aider) 434241149b feat: Add comprehensive logging for Google Drive file upload process 2024-12-15 19:13:29 -05:00
Taylor Wilsdon (aider) 9b939e99f2 refactor: Update Google Drive file upload to match local file upload flow 2024-12-15 19:12:33 -05:00
Taylor Wilsdon (aider) 9faa5856f5 fix: Update Google Drive Picker to show files and improve file upload process 2024-12-15 19:10:41 -05:00
Taylor Wilsdon (aider) b57f7251a5 feat: Improve Google Drive file upload handling with better logging and error management 2024-12-15 19:06:38 -05:00
Taylor Wilsdon (aider) b9499b4392 fix: Resolve Google Drive Picker 404 error and improve configuration 2024-12-15 19:04:44 -05:00
Taylor Wilsdon (aider) 29efee8ede feat: Improve Google Drive file upload with correct headers 2024-12-15 18:57:59 -05:00
Taylor Wilsdon (aider) 7d55f9bc2e fix: Adjust Google Drive file upload headers in processWeb call 2024-12-15 18:54:05 -05:00
Taylor Wilsdon (aider) 7dace30587 fix: Update Google Drive file upload to use authorization headers 2024-12-15 18:52:24 -05:00
Taylor Wilsdon (aider) 4adcd2b64a feat: Add comprehensive logging to Google Drive picker integration 2024-12-15 18:51:21 -05:00
Taylor Wilsdon (aider) b458383b82 feat: Add logging for file upload process in MessageInput component 2024-12-15 18:48:43 -05:00
Taylor Wilsdon (aider) 3ce4e36ab2 feat: Add comprehensive console logging for file upload process 2024-12-15 18:48:35 -05:00
Taylor Wilsdon (aider) 713bedf3b3 fix: Simplify Google Drive Picker API loading to resolve 404 error 2024-12-15 18:46:32 -05:00
Taylor Wilsdon (aider) aaacd28131 fix: Resolve Google Drive picker API loading and token handling 2024-12-15 18:45:40 -05:00
Taylor Wilsdon (aider) 3378f35296 fix: Import getAuthToken in MessageInput.svelte to resolve Google Drive upload error 2024-12-15 18:43:06 -05:00
Taylor Wilsdon (aider) 64c8bbc16a feat: Improve Google Drive file download handling 2024-12-15 18:42:26 -05:00
Taylor Wilsdon (aider) eef18d4440 fix: Properly structure Google Drive file upload data for dispatch 2024-12-15 18:39:23 -05:00
Taylor Wilsdon (aider) 7d31b111cc feat: Add folder selection and app ID to Google Drive picker 2024-12-15 18:33:40 -05:00
Taylor Wilsdon (aider) ba19ff8ace fix: Improve Google OAuth token retrieval with proper Promise handling 2024-12-15 18:31:58 -05:00
Taylor Wilsdon (aider) c3d631ca98 fix: Improve Google Drive API credentials validation and error handling 2024-12-15 16:55:04 -05:00
Taylor Wilsdon (aider) a0ba5974f6 feat: Add credential validation for Google Drive API initialization 2024-12-15 16:54:19 -05:00
Taylor Wilsdon (aider) f29dc2f865 fix: Add missing import for createPicker in MessageInput.svelte 2024-12-15 16:53:07 -05:00
Taylor Wilsdon (aider) 85508106a8 fix: Improve Google Drive picker initialization and error handling 2024-12-15 16:36:45 -05:00
Taylor Wilsdon (aider) eed7cfd2a2 refactor: Remove unused Google Drive picker imports from Chat.svelte 2024-12-15 16:34:26 -05:00
Taylor Wilsdon (aider) b2dc6fef9f fix: Improve Google Drive picker API loading and error handling 2024-12-15 16:33:15 -05:00
Taylor Wilsdon (aider) 1cd43b122b feat: Import createPicker function in InputMenu component 2024-12-15 16:31:58 -05:00
Taylor Wilsdon (aider) 7bc1876e37 fix: Resolve Google Drive picker promise with file data 2024-12-15 16:31:30 -05:00
Taylor Wilsdon (aider) 90e70608b9 feat: Add Google Drive upload option to InputMenu 2024-12-15 16:25:18 -05:00
Taylor Wilsdon (aider) f566c5940a feat: Add Google Drive picker button to MessageInput component 2024-12-15 16:08:32 -05:00
Taylor Wilsdon (aider) 61b1a8fdab feat: Add Google Drive file picker integration to chat interface 2024-12-15 16:07:43 -05:00
Michal Hudak 8c30c6282b added slovak translation file 2024-12-15 13:25:16 +01:00
Michal Hudak 62c6ebe55a Added Slovak langage, improved Czech language 2024-12-15 13:20:51 +01:00
i0ntempest c0f106836f Allow installing with python 3.12 2024-12-14 18:09:10 +08:00
Gabriel Ecegi 440894f8d3 Fix process/files/batch 2024-12-14 10:45:27 +01:00
Timothy Jaeryang Baek 6efca03a8f refac 2024-12-13 22:51:43 -08:00
Juanan Pereira 6f3ab5917d fix: use WEBUI_API_BASE_URL for file download links in Citations 2024-12-13 15:42:45 +01:00
Gabriel Ecegi f2e2b59c18 Add batching 2024-12-13 15:29:43 +01:00
Timothy Jaeryang Baek f9a05dd1e1 refac 2024-12-12 23:31:08 -08:00
Timothy Jaeryang Baek 9a081c8593 refac 2024-12-12 22:32:28 -08:00
Timothy Jaeryang Baek 1197c640c4 refac 2024-12-12 22:28:42 -08:00
Timothy Jaeryang Baek 8c38708827 wip 2024-12-12 20:26:28 -08:00
Timothy Jaeryang Baek d8a01cb911 wip 2024-12-12 20:24:36 -08:00
Timothy Jaeryang Baek 4311bb7b99 wip 2024-12-12 20:22:17 -08:00
Марко М. Костић (Marko M. Kostić) 6aaede510b Update Serbian translation 2024-12-12 14:36:39 +01:00
Timothy Jaeryang Baek 403262d764 fix 2024-12-11 20:40:20 -08:00
Timothy Jaeryang Baek 866c3dff11 fix 2024-12-11 20:39:55 -08:00
Timothy Jaeryang Baek d9ffcea764 wip 2024-12-11 20:26:24 -08:00
Timothy Jaeryang Baek eb9733e99f wip 2024-12-11 20:25:46 -08:00
Timothy Jaeryang Baek a07ff56c50 wip 2024-12-11 20:15:23 -08:00
Timothy Jaeryang Baek fe5519e0a2 wip 2024-12-11 19:52:46 -08:00
Timothy Jaeryang Baek 772f5ccd60 wip 2024-12-11 18:53:38 -08:00
Timothy Jaeryang Baek ccdf51588e wip 2024-12-11 18:46:29 -08:00
Timothy Jaeryang Baek 3bda1a8b88 wip 2024-12-11 18:36:59 -08:00
Timothy Jaeryang Baek 9e85ed861d wip: pipelines 2024-12-11 18:16:07 -08:00
Timothy Jaeryang Baek b3987ad41e wip 2024-12-11 18:08:55 -08:00
Timothy Jaeryang Baek 867c4bc0d0 wip: retrieval 2024-12-11 18:05:42 -08:00
Timothy Jaeryang Baek 3ec0a58cd7 wip 2024-12-11 17:50:48 -08:00
Timothy Jaeryang Baek bfdbb2df69 fix: rich text input issue 2024-12-11 14:07:25 -08:00
Timothy Jaeryang Baek 87d695caad Update audio.py 2024-12-11 04:47:35 -08:00
Timothy Jaeryang Baek df0cdd9f3c wip 2024-12-11 04:37:47 -08:00
oyve 2f2bd88dd1 Merge remote-tracking branch 'upstream/dev' into dev 2024-12-11 12:54:17 +01:00
Timothy Jaeryang Baek df48eac22b wip 2024-12-11 03:38:45 -08:00
Timothy Jaeryang Baek 4819199650 wip 2024-12-11 02:41:25 -08:00
MadsLang 332188535e add option to add database schema as env var 2024-12-10 15:18:26 +01:00
Timothy Jaeryang Baek d3d161f723 wip 2024-12-10 00:54:13 -08:00
Sanchit Hira edf6c6a18c turn off auto update in offline mode 2024-12-10 00:17:49 -08:00
Timothy Jaeryang Baek a495f68b58 fix: textarea styling 2024-12-10 00:01:19 -08:00
Timothy Jaeryang Baek f6bec8d9f3 general refac 2024-12-10 00:00:01 -08:00
Timothy Jaeryang Baek 1349c6049e fix: BYPASS_MODEL_ACCESS_CONTROL env var 2024-12-09 23:39:23 -08:00
Timothy Jaeryang Baek 2db837cab4 fix: query_embedding param 2024-12-09 23:33:43 -08:00
Timothy Jaeryang Baek 8de91df1ff refac: rm print tags 2024-12-09 23:30:57 -08:00
Timothy Jaeryang Baek 6b46b8bf62 refac: rm print 2024-12-09 23:30:43 -08:00
Timothy Jaeryang Baek 10747a6b04 fix: clear files when saving response message as new 2024-12-09 23:24:45 -08:00
Timothy Jaeryang Baek faa054d4b4 fix: comfyui cfg setting 2024-12-09 23:22:47 -08:00
Timothy Jaeryang Baek 9ddb16345f refac 2024-12-09 23:02:11 -08:00
Timothy Jaeryang Baek f264d82d13 Merge pull request #7551 from jonassvatos/patch-1
feat: Add OAUTH_ALLOWED_DOMAINS
2024-12-09 16:27:06 -08:00
Timothy Jaeryang Baek 8718067894 Merge pull request #7678 from ZaibanAli/feature/keycloak-terminate-sso-session
feat: implement OAuth logout functionality for keyclock to terminate sso session
2024-12-09 16:26:10 -08:00
Timothy Jaeryang Baek a3ca632921 refac: id_token -> oauth_id_token 2024-12-09 16:25:56 -08:00
Timothy Jaeryang Baek d4d6d1e7db Merge pull request #7694 from erics118/main
feat: add kagi search engine
2024-12-09 16:12:32 -08:00
Timothy Jaeryang Baek a4ed027498 Merge pull request #7696 from ishiland/main
feat: add ollama ps endpoint
2024-12-09 16:12:07 -08:00
Timothy Jaeryang Baek 56286a2157 Merge pull request #7697 from marcusziade/improve-finnish-translations
i18n: Improve Finnish translations
2024-12-09 16:11:25 -08:00
Timothy Jaeryang Baek 961c1efe38 Merge pull request #7728 from OriginalSimon/dev
18n: Update Ukrainian translation
2024-12-09 16:11:09 -08:00
Timothy Jaeryang Baek 43b791927e refac: rm print 2024-12-09 16:09:21 -08:00
Simon 7cbad465e5 Update translation.json 2024-12-09 21:42:50 +01:00
Timothy Jaeryang Baek 33099bf9e4 refac 2024-12-08 16:01:56 -08:00
Marcus Ziadé 1dadfa9f97 Update Finnish translations in translation.json file 2024-12-08 17:38:26 +02:00
Ian Shiland 9c554db37c add ollama ps endpoint 2024-12-08 07:49:32 -05:00
erics118 b825947745 feat: add kagi 2024-12-08 00:21:10 -05:00
Zaiban Ali 899424b371 feat: refactor signout functionality to use aiohttp for OpenID configuration retrieval 2024-12-08 04:57:57 +01:00
Timothy Jaeryang Baek 8dcee6b6ed refac: pdf 2024-12-07 14:28:17 -08:00
Timothy Jaeryang Baek 1439f6862d enh: ollama /v1/completion endpoint support 2024-12-07 13:46:46 -08:00
Zaiban Ali 48d604a525 feat: enable OAuth signup configuration for signout functionality 2024-12-07 15:21:05 +01:00
Zaiban Ali 9918ec6246 feat: update signout functionality to use OpenID configuration for logout URL and remove the logout variable from config 2024-12-07 15:13:13 +01:00
Zaiban Ali d5ce85f34a feat: implement OAuth logout functionality for keyclock to terminate sso session 2024-12-07 13:49:12 +01:00
Timothy Jaeryang Baek 29a2719595 Merge pull request #7562 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.8
2024-12-07 00:42:50 -08:00
Timothy Jaeryang Baek 976676a482 chore: format 2024-12-07 00:34:52 -08:00
Timothy Jaeryang Baek 19ad8b4f26 doc: changelog 2024-12-07 00:29:23 -08:00
Timothy Jaeryang Baek f04767f1fe Merge pull request #7655 from aleixdorca/dev
i18n: Update catalan translation.json
2024-12-06 12:01:38 -08:00
Timothy Jaeryang Baek 36085f3036 refac 2024-12-06 11:55:25 -08:00
Aleix Dorca 4a46417275 Update catalan translation.json 2024-12-06 15:02:54 +01:00
Timothy Jaeryang Baek a68e717dcf Update CHANGELOG.md 2024-12-05 14:11:52 -08:00
Timothy Jaeryang Baek 346b7ddb14 refac: tailwind 2024-12-05 10:42:37 -08:00
Timothy Jaeryang Baek 31545439a0 Merge pull request #7590 from HUD4K/cs-CZ-translation
Cs cz translation
2024-12-05 10:41:55 -08:00
Timothy Jaeryang Baek 5401b24717 Merge pull request #7589 from AliveDedSec/main
Updated and expanded translation into Russian
2024-12-05 10:41:44 -08:00
Michal Hudak 152b1224a8 updated changelog 2024-12-04 09:55:34 +01:00
Michal Hudak ac5ea4ff25 Updated and improved cs-CZ translation 2024-12-04 09:30:08 +01:00
SVAROG 6e73b3d075 Update translation.json 2024-12-04 07:50:43 +03:00
SVAROG 054a718da0 Update translation.json 2024-12-04 07:43:46 +03:00
SVAROG a63326b550 Update translation.json 2024-12-04 07:37:10 +03:00
SVAROG 547d17c362 Update translation.json 2024-12-04 07:35:35 +03:00
SVAROG 10382998f1 Update translation.json 2024-12-04 07:24:14 +03:00
SVAROG c5f7ad2009 Update translation.json 2024-12-04 06:56:30 +03:00
SVAROG 6173636a11 Update translation.json 2024-12-04 06:26:55 +03:00
Timothy Jaeryang Baek 9b6076f726 Merge pull request #7549 from panda44312/dev
i18n: Update Traditional Chinese translation
2024-12-02 23:42:34 -08:00
Mandy Schoep 8fcb08c541 feat: add HCL highlighting 2024-12-02 11:08:02 +01:00
jonassvatos d42de65298 Add OAUTH_ALLOWED_DOMAINS for ability to restrict from which e-mail domains can users sign-up via OAuth 2024-12-02 10:23:05 +01:00
Panda abbf37f3d9 Update translation.json 2024-12-02 09:49:02 +01:00
Timothy Jaeryang Baek 5433340bb1 enh: banner md support 2024-12-01 23:58:22 -08:00
Timothy Jaeryang Baek 1dab0cfada refac 2024-12-01 23:35:24 -08:00
Timothy Jaeryang Baek 2714a1bb64 refac: styling 2024-12-01 23:29:21 -08:00
Timothy Jaeryang Baek 4053de5825 refac: styling 2024-12-01 23:16:00 -08:00
Timothy Jaeryang Baek 29573afb91 Merge pull request #7536 from marvinvr/main
style: Add proper favicon and iOS home screen support
2024-12-01 22:21:33 -08:00
Marvin von Rappard 39b7e54482 feat: properly format favicons and make home screen icons look better 2024-12-02 07:06:32 +01:00
Timothy Jaeryang Baek 7f77828e3f refac: styling 2024-12-01 21:50:21 -08:00
Timothy Jaeryang Baek 59c3a18118 enh: BYPASS_MODEL_ACCESS_CONTROL 2024-12-01 18:25:44 -08:00
Timothy Jaeryang Baek 460992613f refac: styling 2024-12-01 17:15:16 -08:00
Timothy Jaeryang Baek 0055f3dcb6 refac: rich text input 2024-12-01 14:49:10 -08:00
Timothy Jaeryang Baek 1c078bdb55 fix: api query collection 2024-12-01 13:36:36 -08:00
Timothy Jaeryang Baek 85d2b898c6 Merge pull request #7512 from panda44312/dev
i18n: Update Simplified Chinese Translation
2024-12-01 12:30:15 -08:00
Timothy Jaeryang Baek b903e3a896 Merge pull request #7507 from TiancongLx/dev
i18n: fix zh-TW
2024-12-01 12:30:05 -08:00
panda44312 aecb7ae398 Update translation.json 2024-12-01 15:43:53 +01:00
Tiancong Li 7f4a83ea0e i18n: fix zh-TW 2024-12-01 18:16:05 +08:00
oyve 665e9cd129 Merge remote-tracking branch 'upstream/dev' into dev 2024-11-27 14:52:20 +01:00
oyve d219b3bfd6 Add and merge latest translations 2024-11-27 13:30:05 +01:00
oyve 47483c4402 Update to latest translations 2024-11-26 11:53:55 +01:00
oyve a06f57c0a5 Fill in blanks 2024-11-20 20:29:42 +01:00
oyve 7589e4f5e5 Merge remote-tracking branch 'upstream/dev' into dev 2024-11-20 20:20:53 +01:00
Øyvind 165ee3649b Polish text
Improve text coherence and ensure it aligns with Norwegian writing rules
2024-11-17 21:32:21 +01:00
Øyvind 5fd511b90b Fill in latest blanks
First translation of new blanks after synching fork
2024-11-17 17:28:15 +01:00
4212 changed files with 120677 additions and 13567 deletions
+25
View File
@@ -0,0 +1,25 @@
# Codespell configuration is within pyproject.toml
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
@@ -52,6 +52,8 @@ jobs:
- name: Cypress run
uses: cypress-io/github-action@v6
env:
LIBGL_ALWAYS_SOFTWARE: 1
with:
browser: chrome
wait-on: 'http://localhost:3000'
+173
View File
@@ -5,6 +5,179 @@ 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.5.7] - 2025-01-23
### Added
- **🌍 Enhanced Internationalization (i18n)**: Refined and expanded translations for greater global accessibility and a smoother experience for international users.
### Fixed
- **🔗 Connection Model ID Resolution**: Resolved an issue preventing model IDs from registering in connections.
- **💡 Prefix ID for Ollama Connections**: Fixed a bug where prefix IDs in Ollama connections were non-functional.
- **🔧 Ollama Model Enable/Disable Functionality**: Addressed the issue of enable/disable toggles not working for Ollama base models.
- **🔒 RBAC Permissions for Tools and Models**: Corrected incorrect Role-Based Access Control (RBAC) permissions for tools and models, ensuring that users now only access features according to their assigned privileges, enhancing security and role clarity.
## [0.5.6] - 2025-01-22
### Added
- **🧠 Effortful Reasoning Control for OpenAI Models**: Introduced the reasoning_effort parameter in chat controls for supported OpenAI models, enabling users to fine-tune how much cognitive effort a model dedicates to its responses, offering greater customization for complex queries and reasoning tasks.
### Fixed
- **🔄 Chat Controls Loading UI Bug**: Resolved an issue where collapsible chat controls appeared as "loading," ensuring a smoother and more intuitive user experience for managing chat settings.
### Changed
- **🔧 Updated Ollama Model Creation**: Revamped the Ollama model creation method to align with their new JSON payload format, ensuring seamless compatibility and more efficient model setup workflows.
## [0.5.5] - 2025-01-22
### Added
- **🤔 Native 'Think' Tag Support**: Introduced the new 'think' tag support that visually displays how long the model is thinking, omitting the reasoning content itself until the next turn. Ideal for creating a more streamlined and focused interaction experience.
- **🖼️ Toggle Image Generation On/Off**: In the chat input menu, you can now easily toggle image generation before initiating chats, providing greater control and flexibility to suit your needs.
- **🔒 Chat Controls Permissions**: Admins can now disable chat controls access for users, offering tighter management and customization over user interactions.
- **🔍 Web Search & Image Generation Permissions**: Easily disable web search and image generation for specific users, improving workflow governance and security for certain environments.
- **🗂️ S3 and GCS Storage Provider Support**: Scaled deployments now benefit from expanded storage options with Amazon S3 and Google Cloud Storage seamlessly integrated as providers.
- **🎨 Enhanced Model Management**: Reintroduced the ability to download and delete models directly in the admin models settings page to minimize user confusion and aid efficient model management.
- **🔗 Improved Connection Handling**: Enhanced backend to smoothly handle multiple identical base URLs, allowing more flexible multi-instance configurations with fewer hiccups.
- **✨ General UI/UX Refinements**: Numerous tweaks across the WebUI make navigation and usability even more user-friendly and intuitive.
- **🌍 Translation Enhancements**: Various translation updates ensure smoother and more polished interactions for international users.
### Fixed
- **⚡ MPS Functionality for Mac Users**: Fixed MPS support, ensuring smooth performance and compatibility for Mac users leveraging MPS.
- **📡 Ollama Connection Management**: Resolved the issue where deleting all Ollama connections prevented adding new ones.
### Changed
- **⚙️ General Stability Refac**: Backend refactoring delivers a more stable, robust platform.
- **🖥️ Desktop App Preparations**: Ongoing work to support the upcoming Open WebUI desktop app. Follow our progress and updates here: https://github.com/open-webui/desktop
## [0.5.4] - 2025-01-05
### Added
- **🔄 Clone Shared Chats**: Effortlessly clone shared chats to save time and streamline collaboration, perfect for reusing insightful discussions or custom setups.
- **📣 Native Notifications for Channel Messages**: Stay informed with integrated desktop notifications for channel messages, ensuring you never miss important updates while multitasking.
- **🔥 Torch MPS Support**: MPS support for Mac users when Open WebUI is installed directly, offering better performance and compatibility for AI workloads.
- **🌍 Enhanced Translations**: Small improvements to various translations, ensuring a smoother global user experience.
### Fixed
- **🖼️ Image-Only Messages in Channels**: You can now send images without accompanying text or content in channels.
- **❌ Proper Exception Handling**: Enhanced error feedback by ensuring exceptions are raised clearly, reducing confusion and promoting smoother debugging.
- **🔍 RAG Query Generation Restored**: Fixed query generation issues for Retrieval-Augmented Generation, improving retrieval accuracy and ensuring seamless functionality.
- **📩 MOA Response Functionality Fixed**: Addressed an error with the MOA response generation feature.
- **💬 Channel Thread Loading with 50+ Messages**: Resolved an issue where channel threads stalled when exceeding 50 messages, ensuring smooth navigation in active discussions.
- **🔑 API Endpoint Restrictions Resolution**: Fixed a critical bug where the 'API_KEY_ALLOWED_ENDPOINTS' setting was not functioning as intended, ensuring API access is limited to specified endpoints for enhanced security.
- **🛠️ Action Functions Restored**: Corrected an issue preventing action functions from working, restoring their utility for customized automations and workflows.
- **📂 Temporary Chat JSON Export Fix**: Resolved a bug blocking temporary chats from being exported in JSON format, ensuring seamless data portability.
### Changed
- **🎛️ Sidebar UI Tweaks**: Chat folders, including pinned folders, now display below the Chats section for better organization; the "New Folder" button has been relocated to the Chats section for a more intuitive workflow.
- **🏗️ Real-Time Save Disabled by Default**: The 'ENABLE_REALTIME_CHAT_SAVE' setting is now off by default, boosting response speed for users who prioritize performance in high-paced workflows or less critical scenarios.
- **🎤 Audio Input Echo Cancellation**: Audio input now features echo cancellation enabled by default, reducing audio feedback for improved clarity during conversations or voice-based interactions.
- **🔧 General Reliability Improvements**: Numerous under-the-hood enhancements have been made to improve platform stability, boost overall performance, and ensure a more seamless, dependable experience across workflows.
## [0.5.3] - 2024-12-31
### Added
- **💬 Channel Reactions with Built-In Emoji Picker**: Easily express yourself in channel threads and messages with reactions, featuring an intuitive built-in emoji picker for seamless selection.
- **🧵 Threads for Channels**: Organize discussions within channels by creating threads, improving clarity and fostering focused conversations.
- **🔄 Reset Button for SVG Pan/Zoom**: Added a handy reset button to SVG Pan/Zoom, allowing users to quickly return diagrams or visuals to their default state without hassle.
- **⚡ Realtime Chat Save Environment Variable**: Introduced the ENABLE_REALTIME_CHAT_SAVE environment variable. Choose between faster responses by disabling realtime chat saving or ensuring chunk-by-chunk data persistency for critical operations.
- **🌍 Translation Enhancements**: Updated and refined translations across multiple languages, providing a smoother experience for international users.
- **📚 Improved Documentation**: Expanded documentation on functions, including clearer guidance on function plugins and detailed instructions for migrating to v0.5. This ensures users can adapt and harness new updates more effectively. (https://docs.openwebui.com/features/plugin/)
### Fixed
- **🛠️ Ollama Parameters Respected**: Resolved an issue where input parameters for Ollama were being ignored, ensuring precise and consistent model behavior.
- **🔧 Function Plugin Outlet Hook Reliability**: Fixed a bug causing issues with 'event_emitter' and outlet hooks in filter function plugins, guaranteeing smoother operation within custom extensions.
- **🖋️ Weird Custom Status Descriptions**: Adjusted the formatting and functionality for custom user statuses, ensuring they display correctly and intuitively.
- **🔗 Restored API Functionality**: Fixed a critical issue where APIs were not operational for certain configurations, ensuring uninterrupted access.
- **⏳ Custom Pipe Function Completion**: Resolved an issue where chats using specific custom pipe function plugins werent finishing properly, restoring consistent chat workflows.
- **✅ General Stability Enhancements**: Implemented various under-the-hood improvements to boost overall reliability, ensuring smoother and more consistent performance across the WebUI.
## [0.5.2] - 2024-12-26
### Added
- **🖊️ Typing Indicators in Channels**: Know exactly whos typing in real-time within your channels, enhancing collaboration and keeping everyone engaged.
- **👤 User Status Indicators**: Quickly view a users status by clicking their profile image in channels for better coordination and availability insights.
- **🔒 Configurable API Key Authentication Restrictions**: Flexibly configure endpoint restrictions for API key authentication, now off by default for a smoother setup in trusted environments.
### Fixed
- **🔧 Playground Functionality Restored**: Resolved a critical issue where the playground wasnt working, ensuring seamless experimentation and troubleshooting workflows.
- **📊 Corrected Ollama Usage Statistics**: Fixed a calculation error in Ollamas usage statistics, providing more accurate tracking and insights for better resource management.
- **🔗 Pipelines Outlet Hook Registration**: Addressed an issue where outlet hooks for pipelines werent registered, restoring functionality and consistency in pipeline workflows.
- **🎨 Image Generation Error**: Resolved a persistent issue causing errors with 'get_automatic1111_api_auth()' to ensure smooth image generation workflows.
- **🎙️ Text-to-Speech Error**: Fixed the missing argument in Eleven Labs 'get_available_voices()', restoring full text-to-speech capabilities for uninterrupted voice interactions.
- **🖋️ Title Generation Issue**: Fixed a bug where title generation was not working in certain cases, ensuring consistent and reliable chat organization.
## [0.5.1] - 2024-12-25
### Added
- **🔕 Notification Sound Toggle**: Added a new setting under Settings > Interface to disable notification sounds, giving you greater control over your workspace environment and focus.
### Fixed
- **🔄 Non-Streaming Response Visibility**: Resolved an issue where non-streaming responses were not displayed, ensuring all responses are now reliably shown in your conversations.
- **🖋️ Title Generation with OpenAI APIs**: Fixed a bug preventing title generation when using OpenAI APIs, restoring the ability to automatically generate chat titles for smoother organization.
- **👥 Admin Panel User List**: Addressed the issue where only 50 users were visible in the admin panel. You can now manage and view all users without restrictions.
- **🖼️ Image Generation Error**: Fixed the issue causing 'get_automatic1111_api_auth()' errors in image generation, ensuring seamless creative workflows.
- **⚙️ Pipeline Settings Loading Issue**: Resolved a problem where pipeline settings were stuck at the loading screen, restoring full configurability in the admin panel.
## [0.5.0] - 2024-12-25
### Added
- **💬 True Asynchronous Chat Support**: Create chats, navigate away, and return anytime with responses ready. Ideal for reasoning models and multi-agent workflows, enhancing multitasking like never before.
- **🔔 Chat Completion Notifications**: Never miss a completed response. Receive instant in-UI notifications when a chat finishes in a non-active tab, keeping you updated while you work elsewhere.
- **🌐 Notification Webhook Integration**: Get alerts via webhooks even when your tab is closed! Configure your webhook URL in Settings > Account and receive timely updates for long-running chats or external integration needs.
- **📚 Channels (Beta)**: Explore Discord/Slack-style chat rooms designed for real-time collaboration between users and AIs. Build bots for channels and unlock asynchronous communication for proactive multi-agent workflows. Opt-in via Admin Settings > General. A Comprehensive Bot SDK tutorial (https://github.com/open-webui/bot) is incoming, so stay tuned!
- **🖼️ Client-Side Image Compression**: Now compress images before upload (Settings > Interface), saving bandwidth and improving performance seamlessly.
- **🛠️ OAuth Management for User Groups**: Enable group-level management via OAuth integration for enhanced control and scalability in collaborative environments.
- **✅ Structured Output for Ollama**: Pass structured data output directly to Ollama, unlocking new possibilities for streamlined automation and precise data handling.
- **📜 Offline Swagger Documentation**: Developer-friendly Swagger API docs are now available offline, ensuring full accessibility wherever you are.
- **📸 Quick Screen Capture Button**: Effortlessly capture your screen with a single click from the message input menu.
- **🌍 i18n Updates**: Improved and refined translations across several languages, including Ukrainian, German, Brazilian Portuguese, Catalan, and more, ensuring a seamless global user experience.
### Fixed
- **📋 Table Export to CSV**: Resolved issues with CSV export where headers were missing or errors occurred due to values with commas, ensuring smooth and reliable data handling.
- **🔓 BYPASS_MODEL_ACCESS_CONTROL**: Fixed an issue where users could see models but couldnt use them with 'BYPASS_MODEL_ACCESS_CONTROL=True', restoring proper functionality for environments leveraging this setting.
### Changed
- **💡 API Key Authentication Restriction**: Narrowed API key auth permissions to '/api/models' and '/api/chat/completions' for enhanced security and better API governance.
- **⚙️ Backend Overhaul for Performance**: Major backend restructuring; a heads-up that some "Functions" using internal variables may face compatibility issues. Moving forward, websocket support is mandatory to ensure Open WebUI operates seamlessly.
### Removed
- **⚠️ Legacy Functionality Clean-Up**: Deprecated outdated backend systems that were non-essential or overlapped with newer implementations, allowing for a leaner, more efficient platform.
## [0.4.8] - 2024-12-07
### Added
- **🔓 Bypass Model Access Control**: Introduced the 'BYPASS_MODEL_ACCESS_CONTROL' environment variable. Easily bypass model access controls for user roles when access control isn't required, simplifying workflows for trusted environments.
- **📝 Markdown in Banners**: Now supports markdown for banners, enabling richer, more visually engaging announcements.
- **🌐 Internationalization Updates**: Enhanced translations across multiple languages, further improving accessibility and global user experience.
- **🎨 Styling Enhancements**: General UI style refinements for a cleaner and more polished interface.
- **📋 Rich Text Reliability**: Improved the reliability and stability of rich text input across chats for smoother interactions.
### Fixed
- **💡 Tailwind Build Issue**: Resolved a breaking bug caused by Tailwind, ensuring smoother builds and overall system reliability.
- **📚 Knowledge Collection Query Fix**: Addressed API endpoint issues with querying knowledge collections, ensuring accurate and reliable information retrieval.
## [0.4.7] - 2024-12-01
### Added
+1 -1
View File
@@ -2,7 +2,7 @@
## Our Pledge
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, 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, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
+23 -17
View File
@@ -1,21 +1,27 @@
MIT License
Copyright (c) 2023-2025 Timothy Jaeryang Baek
All rights reserved.
Copyright (c) 2023 Timothy Jaeryang Baek
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+12 -2
View File
@@ -11,7 +11,9 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
**Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
![Open WebUI Demo](./demo.gif)
@@ -185,13 +187,21 @@ If you want to try out the latest bleeding-edge features and are okay with occas
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
```
### Offline Mode
If you are running Open WebUI in an offline environment, you can set the `HF_HUB_OFFLINE` environment variable to `1` to prevent attempts to download models from the internet.
```bash
export HF_HUB_OFFLINE=1
```
## What's Next? 🌟
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
## License 📜
This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
This project is licensed under the [BSD-3-Clause License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
## Support 💬
+19
View File
@@ -5,12 +5,31 @@ from pathlib import Path
import typer
import uvicorn
from typing import Optional
from typing_extensions import Annotated
app = typer.Typer()
KEY_FILE = Path.cwd() / ".webui_secret_key"
def version_callback(value: bool):
if value:
from open_webui.env import VERSION
typer.echo(f"Open WebUI version: {VERSION}")
raise typer.Exit()
@app.command()
def main(
version: Annotated[
Optional[bool], typer.Option("--version", callback=version_callback)
] = None,
):
pass
@app.command()
def serve(
host: str = "0.0.0.0",
-703
View File
@@ -1,703 +0,0 @@
import hashlib
import json
import logging
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,
AUDIO_STT_MODEL,
AUDIO_STT_OPENAI_API_BASE_URL,
AUDIO_STT_OPENAI_API_KEY,
AUDIO_TTS_API_KEY,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
AUDIO_TTS_OPENAI_API_BASE_URL,
AUDIO_TTS_OPENAI_API_KEY,
AUDIO_TTS_SPLIT_ON,
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
CACHE_DIR,
CORS_ALLOW_ORIGIN,
WHISPER_MODEL,
WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR,
AppConfig,
)
from open_webui.constants import ERROR_MESSAGES
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_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(
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,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.state.config = AppConfig()
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
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
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
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
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
log.info(f"whisper_device_type: {whisper_device_type}")
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
API_KEY: str
ENGINE: str
MODEL: str
VOICE: str
SPLIT_ON: str
AZURE_SPEECH_REGION: str
AZURE_SPEECH_OUTPUT_FORMAT: str
class STTConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
WHISPER_MODEL: str
class AudioConfigUpdateForm(BaseModel):
tts: TTSConfigForm
stt: STTConfigForm
from pydub import AudioSegment
from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
if not os.path.isfile(file_path):
print(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return True
return False
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav")
print(f"Converted {file_path} to {output_path}")
@app.get("/config")
async def get_audio_config(user=Depends(get_admin_user)):
return {
"tts": {
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": app.state.config.TTS_API_KEY,
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
"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,
},
}
@app.post("/config/update")
async def update_audio_config(
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
):
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
app.state.config.TTS_API_KEY = form_data.tts.API_KEY
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
app.state.config.TTS_MODEL = form_data.tts.MODEL
app.state.config.TTS_VOICE = form_data.tts.VOICE
app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
)
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
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": {
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": app.state.config.TTS_API_KEY,
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
"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()
name = hashlib.sha256(body).hexdigest()
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
# Check if the file already exists in the cache
if file_path.is_file():
return FileResponse(file_path)
if app.state.config.TTS_ENGINE == "openai":
headers = {}
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)
body["model"] = app.state.config.TTS_MODEL
body = json.dumps(body).encode("utf-8")
except Exception:
pass
try:
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())
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
return FileResponse(file_path)
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
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}"
raise HTTPException(
status_code=getattr(r, "status", 500),
detail=error_detail,
)
elif app.state.config.TTS_ENGINE == "elevenlabs":
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
log.exception(e)
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,
detail="Invalid voice id",
)
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,
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
}
try:
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())
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(json.loads(body.decode("utf-8"))))
return FileResponse(file_path)
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
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}"
raise HTTPException(
status_code=getattr(r, "status", 500),
detail=error_detail,
)
elif app.state.config.TTS_ENGINE == "azure":
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
region = app.state.config.TTS_AZURE_SPEECH_REGION
language = app.state.config.TTS_VOICE
locale = "-".join(app.state.config.TTS_VOICE.split("-")[:1])
output_format = app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
headers = {
"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY,
"Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": output_format,
}
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
<voice name="{language}">{payload["input"]}</voice>
</speak>"""
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")
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 transcription(
file: UploadFile = File(...),
user=Depends(get_verified_user),
):
log.info(f"file.content_type: {file.content_type}")
if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
)
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}"
with open(file_path, "wb") as f:
f.write(contents)
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
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"
),
)
data = transcribe(file_path)
else:
data = transcribe(file_path)
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),
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def get_available_models() -> list[dict]:
if app.state.config.TTS_ENGINE == "openai":
return [{"id": "tts-1"}, {"id": "tts-1-hd"}]
elif app.state.config.TTS_ENGINE == "elevenlabs":
headers = {
"xi-api-key": app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
}
try:
response = requests.get(
"https://api.elevenlabs.io/v1/models", headers=headers, timeout=5
)
response.raise_for_status()
models = response.json()
return [
{"name": model["name"], "id": model["model_id"]} for model in models
]
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return []
@app.get("/models")
async def get_models(user=Depends(get_verified_user)):
return {"models": get_available_models()}
def get_available_voices() -> dict:
"""Returns {voice_id: voice_name} dict"""
ret = {}
if app.state.config.TTS_ENGINE == "openai":
ret = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
elif app.state.config.TTS_ENGINE == "elevenlabs":
try:
ret = get_elevenlabs_voices()
except Exception:
# Avoided @lru_cache with exception
pass
elif app.state.config.TTS_ENGINE == "azure":
try:
region = app.state.config.TTS_AZURE_SPEECH_REGION
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
headers = {"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY}
response = requests.get(url, headers=headers)
response.raise_for_status()
voices = response.json()
for voice in voices:
ret[voice["ShortName"]] = (
f"{voice['DisplayName']} ({voice['ShortName']})"
)
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return ret
@lru_cache
def get_elevenlabs_voices() -> dict:
"""
Note, set the following in your .env file to use Elevenlabs:
AUDIO_TTS_ENGINE=elevenlabs
AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key
AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices
AUDIO_TTS_MODEL=eleven_multilingual_v2
"""
headers = {
"xi-api-key": app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
}
try:
# TODO: Add retries
response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
response.raise_for_status()
voices_data = response.json()
voices = {}
for voice in voices_data.get("voices", []):
voices[voice["voice_id"]] = voice["name"]
except requests.RequestException as e:
# Avoid @lru_cache with exception
log.error(f"Error fetching voices: {str(e)}")
raise RuntimeError(f"Error fetching voices: {str(e)}")
return voices
@app.get("/voices")
async def get_voices(user=Depends(get_verified_user)):
return {"voices": [{"id": k, "name": v} for k, v in get_available_voices().items()]}
File diff suppressed because it is too large Load Diff
@@ -1,22 +0,0 @@
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()
-221
View File
@@ -1,221 +0,0 @@
# TODO: move socket to webui app
import asyncio
import socketio
import logging
import sys
import time
from open_webui.apps.webui.models.users import Users
from open_webui.env import (
ENABLE_WEBSOCKET_SUPPORT,
WEBSOCKET_MANAGER,
WEBSOCKET_REDIS_URL,
)
from open_webui.utils.utils import decode_token
from open_webui.apps.socket.utils import RedisDict
from open_webui.env import (
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["SOCKET"])
if WEBSOCKET_MANAGER == "redis":
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
sio = socketio.AsyncServer(
cors_allowed_origins=[],
async_mode="asgi",
transports=(
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
),
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
always_connect=True,
client_manager=mgr,
)
else:
sio = socketio.AsyncServer(
cors_allowed_origins=[],
async_mode="asgi",
transports=(
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
),
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
always_connect=True,
)
# Dictionary to maintain the user pool
if WEBSOCKET_MANAGER == "redis":
SESSION_POOL = RedisDict("open-webui:session_pool", redis_url=WEBSOCKET_REDIS_URL)
USER_POOL = RedisDict("open-webui:user_pool", redis_url=WEBSOCKET_REDIS_URL)
USAGE_POOL = RedisDict("open-webui:usage_pool", redis_url=WEBSOCKET_REDIS_URL)
else:
SESSION_POOL = {}
USER_POOL = {}
USAGE_POOL = {}
# Timeout duration in seconds
TIMEOUT_DURATION = 3
async def periodic_usage_pool_cleanup():
while True:
now = int(time.time())
for model_id, connections in list(USAGE_POOL.items()):
# Creating a list of sids to remove if they have timed out
expired_sids = [
sid
for sid, details in connections.items()
if now - details["updated_at"] > TIMEOUT_DURATION
]
for sid in expired_sids:
del connections[sid]
if not connections:
log.debug(f"Cleaning up model {model_id} from usage pool")
del USAGE_POOL[model_id]
else:
USAGE_POOL[model_id] = connections
# Emit updated usage information after cleaning
await sio.emit("usage", {"models": get_models_in_use()})
await asyncio.sleep(TIMEOUT_DURATION)
app = socketio.ASGIApp(
sio,
socketio_path="/ws/socket.io",
)
def get_models_in_use():
# List models that are currently in use
models_in_use = list(USAGE_POOL.keys())
return models_in_use
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
@sio.event
async def connect(sid, environ, auth):
user = None
if auth and "token" in auth:
data = decode_token(auth["token"])
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
if user:
SESSION_POOL[sid] = user.id
if user.id in USER_POOL:
USER_POOL[user.id].append(sid)
else:
USER_POOL[user.id] = [sid]
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(USER_POOL.items())})
await sio.emit("usage", {"models": get_models_in_use()})
@sio.on("user-join")
async def user_join(sid, data):
# print("user-join", sid, data)
auth = data["auth"] if "auth" in data else None
if not auth or "token" not in auth:
return
data = decode_token(auth["token"])
if data is None or "id" not in data:
return
user = Users.get_user_by_id(data["id"])
if not user:
return
SESSION_POOL[sid] = user.id
if user.id in USER_POOL:
USER_POOL[user.id].append(sid)
else:
USER_POOL[user.id] = [sid]
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(USER_POOL.items())})
@sio.on("user-count")
async def user_count(sid):
await sio.emit("user-count", {"count": len(USER_POOL.items())})
@sio.event
async def disconnect(sid):
if sid in SESSION_POOL:
user_id = SESSION_POOL[sid]
del SESSION_POOL[sid]
USER_POOL[user_id] = [_sid for _sid in USER_POOL[user_id] if _sid != sid]
if len(USER_POOL[user_id]) == 0:
del USER_POOL[user_id]
await sio.emit("user-count", {"count": len(USER_POOL)})
else:
pass
# print(f"Unknown session ID {sid} disconnected")
def get_event_emitter(request_info):
async def __event_emitter__(event_data):
await sio.emit(
"chat-events",
{
"chat_id": request_info["chat_id"],
"message_id": request_info["message_id"],
"data": event_data,
},
to=request_info["session_id"],
)
return __event_emitter__
def get_event_call(request_info):
async def __event_call__(event_data):
response = await sio.call(
"chat-events",
{
"chat_id": request_info["chat_id"],
"message_id": request_info["message_id"],
"data": event_data,
},
to=request_info["session_id"],
)
return response
return __event_call__
-506
View File
@@ -1,506 +0,0 @@
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
from open_webui.apps.webui.models.functions import Functions
from open_webui.apps.webui.models.models import Models
from open_webui.apps.webui.routers import (
auths,
chats,
folders,
configs,
groups,
files,
functions,
memories,
models,
knowledge,
prompts,
evaluations,
tools,
users,
utils,
)
from open_webui.apps.webui.utils import load_function_module_by_id
from open_webui.config import (
ADMIN_EMAIL,
CORS_ALLOW_ORIGIN,
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,
)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from open_webui.utils.misc import (
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
)
from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from open_webui.utils.tools import get_tools
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
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
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.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 = {}
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
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(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(functions.router, prefix="/functions", tags=["functions"])
app.include_router(evaluations.router, prefix="/evaluations", tags=["evaluations"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@app.get("/")
async def get_status():
return {
"status": True,
"auth": WEBUI_AUTH,
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
}
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:
function_module, _, _ = load_function_module_by_id(pipe_id)
app.state.FUNCTIONS[pipe_id] = function_module
else:
function_module = app.state.FUNCTIONS[pipe_id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe_id)
function_module.valves = function_module.Valves(**(valves if valves else {}))
return function_module
async def get_pipe_models():
pipes = Functions.get_functions_by_type("pipe", active_only=True)
pipe_models = []
for pipe in pipes:
function_module = get_function_module(pipe.id)
# Check if function is a manifold
if hasattr(function_module, "pipes"):
sub_pipes = []
# Check if pipes is a function or a list
try:
if callable(function_module.pipes):
sub_pipes = function_module.pipes()
else:
sub_pipes = function_module.pipes
except Exception as e:
log.exception(e)
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"]}'
sub_pipe_name = p["name"]
if hasattr(function_module, "name"):
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
pipe_flag = {"type": pipe.type}
pipe_models.append(
{
"id": sub_pipe_id,
"name": sub_pipe_name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
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,
"name": pipe.name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
return pipe_models
async def execute_pipe(pipe, params):
if inspect.iscoroutinefunction(pipe):
return await pipe(**params)
else:
return pipe(**params)
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
if isinstance(res, str):
return res
if isinstance(res, Generator):
return "".join(map(str, res))
if isinstance(res, AsyncGenerator):
return "".join([str(stream) async for stream in res])
def process_line(form_data: dict, line):
if isinstance(line, BaseModel):
line = line.model_dump_json()
line = f"data: {line}"
if isinstance(line, dict):
line = f"data: {json.dumps(line)}"
try:
line = line.decode("utf-8")
except Exception:
pass
if line.startswith("data:"):
return f"{line}\n\n"
else:
line = openai_chat_chunk_message_template(form_data["model"], line)
return f"data: {json.dumps(line)}\n\n"
def get_pipe_id(form_data: dict) -> str:
pipe_id = form_data["model"]
if "." in pipe_id:
pipe_id, _ = pipe_id.split(".", 1)
return pipe_id
def get_function_params(function_module, form_data, user, extra_params=None):
if extra_params is None:
extra_params = {}
pipe_id = get_pipe_id(form_data)
# Get the signature of the function
sig = inspect.signature(function_module.pipe)
params = {"body": form_data} | {
k: v for k, v in extra_params.items() if k in sig.parameters
}
if "__user__" in params and hasattr(function_module, "UserValves"):
user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
try:
params["__user__"]["valves"] = function_module.UserValves(**user_valves)
except Exception as e:
log.exception(e)
params["__user__"]["valves"] = function_module.UserValves()
return params
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)
metadata = form_data.pop("metadata", {})
files = metadata.get("files", [])
tool_ids = metadata.get("tool_ids", [])
# Check if tool_ids is None
if tool_ids is None:
tool_ids = []
__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,
"email": user.email,
"name": user.name,
"role": user.role,
},
"__metadata__": metadata,
}
extra_params["__tools__"] = get_tools(
app,
tool_ids,
user,
{
**extra_params,
"__model__": models.get(form_data["model"], None),
"__messages__": form_data["messages"],
"__files__": files,
},
)
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
params = model_info.params.model_dump()
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(params, form_data, user)
pipe_id = get_pipe_id(form_data)
function_module = get_function_module(pipe_id)
pipe = function_module.pipe
params = get_function_params(function_module, form_data, user, extra_params)
if form_data.get("stream", False):
async def stream_content():
try:
res = await execute_pipe(pipe, params)
# Directly return if the response is a StreamingResponse
if isinstance(res, StreamingResponse):
async for data in res.body_iterator:
yield data
return
if isinstance(res, dict):
yield f"data: {json.dumps(res)}\n\n"
return
except Exception as e:
log.error(f"Error: {e}")
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
return
if isinstance(res, str):
message = openai_chat_chunk_message_template(form_data["model"], res)
yield f"data: {json.dumps(message)}\n\n"
if isinstance(res, Iterator):
for line in res:
yield process_line(form_data, line)
if isinstance(res, AsyncGenerator):
async for line in res:
yield process_line(form_data, line)
if isinstance(res, str) or isinstance(res, Generator):
finish_message = openai_chat_chunk_message_template(
form_data["model"], ""
)
finish_message["choices"][0]["finish_reason"] = "stop"
yield f"data: {json.dumps(finish_message)}\n\n"
yield "data: [DONE]"
return StreamingResponse(stream_content(), media_type="text/event-stream")
else:
try:
res = await execute_pipe(pipe, params)
except Exception as e:
log.error(f"Error: {e}")
return {"error": {"detail": str(e)}}
if isinstance(res, StreamingResponse) or isinstance(res, dict):
return res
if isinstance(res, BaseModel):
return res.model_dump()
message = await get_message_content(res)
return openai_chat_completion_message_template(form_data["model"], message)
+308 -37
View File
@@ -9,21 +9,22 @@ from urllib.parse import urlparse
import chromadb
import requests
import yaml
from open_webui.apps.webui.internal.db import Base, get_db
from pydantic import BaseModel
from sqlalchemy import JSON, Column, DateTime, Integer, func
from open_webui.env import (
OPEN_WEBUI_DIR,
DATA_DIR,
DATABASE_URL,
ENV,
FRONTEND_BUILD_DIR,
OFFLINE_MODE,
OPEN_WEBUI_DIR,
WEBUI_AUTH,
WEBUI_FAVICON_URL,
WEBUI_NAME,
log,
DATABASE_URL,
)
from pydantic import BaseModel
from sqlalchemy import JSON, Column, DateTime, Integer, func
from open_webui.internal.db import Base, get_db
class EndpointFilter(logging.Filter):
@@ -271,6 +272,18 @@ ENABLE_API_KEY = PersistentConfig(
os.environ.get("ENABLE_API_KEY", "True").lower() == "true",
)
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS = PersistentConfig(
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS",
"auth.api_key.endpoint_restrictions",
os.environ.get("ENABLE_API_KEY_ENDPOINT_RESTRICTIONS", "False").lower() == "true",
)
API_KEY_ALLOWED_ENDPOINTS = PersistentConfig(
"API_KEY_ALLOWED_ENDPOINTS",
"auth.api_key.allowed_endpoints",
os.environ.get("API_KEY_ALLOWED_ENDPOINTS", ""),
)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
@@ -306,6 +319,7 @@ GOOGLE_CLIENT_SECRET = PersistentConfig(
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
)
GOOGLE_OAUTH_SCOPE = PersistentConfig(
"GOOGLE_OAUTH_SCOPE",
"oauth.google.scope",
@@ -348,6 +362,30 @@ MICROSOFT_REDIRECT_URI = PersistentConfig(
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
)
GITHUB_CLIENT_ID = PersistentConfig(
"GITHUB_CLIENT_ID",
"oauth.github.client_id",
os.environ.get("GITHUB_CLIENT_ID", ""),
)
GITHUB_CLIENT_SECRET = PersistentConfig(
"GITHUB_CLIENT_SECRET",
"oauth.github.client_secret",
os.environ.get("GITHUB_CLIENT_SECRET", ""),
)
GITHUB_CLIENT_SCOPE = PersistentConfig(
"GITHUB_CLIENT_SCOPE",
"oauth.github.scope",
os.environ.get("GITHUB_CLIENT_SCOPE", "user:email"),
)
GITHUB_CLIENT_REDIRECT_URI = PersistentConfig(
"GITHUB_CLIENT_REDIRECT_URI",
"oauth.github.redirect_uri",
os.environ.get("GITHUB_CLIENT_REDIRECT_URI", ""),
)
OAUTH_CLIENT_ID = PersistentConfig(
"OAUTH_CLIENT_ID",
"oauth.oidc.client_id",
@@ -402,12 +440,24 @@ OAUTH_EMAIL_CLAIM = PersistentConfig(
os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
)
OAUTH_GROUPS_CLAIM = PersistentConfig(
"OAUTH_GROUPS_CLAIM",
"oauth.oidc.group_claim",
os.environ.get("OAUTH_GROUP_CLAIM", "groups"),
)
ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig(
"ENABLE_OAUTH_ROLE_MANAGEMENT",
"oauth.enable_role_mapping",
os.environ.get("ENABLE_OAUTH_ROLE_MANAGEMENT", "False").lower() == "true",
)
ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig(
"ENABLE_OAUTH_GROUP_MANAGEMENT",
"oauth.enable_group_mapping",
os.environ.get("ENABLE_OAUTH_GROUP_MANAGEMENT", "False").lower() == "true",
)
OAUTH_ROLES_CLAIM = PersistentConfig(
"OAUTH_ROLES_CLAIM",
"oauth.roles_claim",
@@ -429,16 +479,33 @@ OAUTH_ADMIN_ROLES = PersistentConfig(
[role.strip() for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(",")],
)
OAUTH_ALLOWED_DOMAINS = PersistentConfig(
"OAUTH_ALLOWED_DOMAINS",
"oauth.allowed_domains",
[
domain.strip()
for domain in os.environ.get("OAUTH_ALLOWED_DOMAINS", "*").split(",")
],
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
def google_oauth_register(client):
client.register(
name="google",
client_id=GOOGLE_CLIENT_ID.value,
client_secret=GOOGLE_CLIENT_SECRET.value,
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": GOOGLE_OAUTH_SCOPE.value},
redirect_uri=GOOGLE_REDIRECT_URI.value,
)
OAUTH_PROVIDERS["google"] = {
"client_id": GOOGLE_CLIENT_ID.value,
"client_secret": GOOGLE_CLIENT_SECRET.value,
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"scope": GOOGLE_OAUTH_SCOPE.value,
"redirect_uri": GOOGLE_REDIRECT_URI.value,
"register": google_oauth_register,
}
if (
@@ -446,12 +513,44 @@ def load_oauth_providers():
and MICROSOFT_CLIENT_SECRET.value
and MICROSOFT_CLIENT_TENANT_ID.value
):
def microsoft_oauth_register(client):
client.register(
name="microsoft",
client_id=MICROSOFT_CLIENT_ID.value,
client_secret=MICROSOFT_CLIENT_SECRET.value,
server_metadata_url=f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
client_kwargs={
"scope": MICROSOFT_OAUTH_SCOPE.value,
},
redirect_uri=MICROSOFT_REDIRECT_URI.value,
)
OAUTH_PROVIDERS["microsoft"] = {
"client_id": MICROSOFT_CLIENT_ID.value,
"client_secret": MICROSOFT_CLIENT_SECRET.value,
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
"scope": MICROSOFT_OAUTH_SCOPE.value,
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
"picture_url": "https://graph.microsoft.com/v1.0/me/photo/$value",
"register": microsoft_oauth_register,
}
if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value:
def github_oauth_register(client):
client.register(
name="github",
client_id=GITHUB_CLIENT_ID.value,
client_secret=GITHUB_CLIENT_SECRET.value,
access_token_url="https://github.com/login/oauth/access_token",
authorize_url="https://github.com/login/oauth/authorize",
api_base_url="https://api.github.com",
userinfo_endpoint="https://api.github.com/user",
client_kwargs={"scope": GITHUB_CLIENT_SCOPE.value},
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
)
OAUTH_PROVIDERS["github"] = {
"redirect_uri": GITHUB_CLIENT_REDIRECT_URI.value,
"register": github_oauth_register,
"sub_claim": "id",
}
if (
@@ -459,13 +558,23 @@ def load_oauth_providers():
and OAUTH_CLIENT_SECRET.value
and OPENID_PROVIDER_URL.value
):
def oidc_oauth_register(client):
client.register(
name="oidc",
client_id=OAUTH_CLIENT_ID.value,
client_secret=OAUTH_CLIENT_SECRET.value,
server_metadata_url=OPENID_PROVIDER_URL.value,
client_kwargs={
"scope": OAUTH_SCOPES.value,
},
redirect_uri=OPENID_REDIRECT_URI.value,
)
OAUTH_PROVIDERS["oidc"] = {
"client_id": OAUTH_CLIENT_ID.value,
"client_secret": OAUTH_CLIENT_SECRET.value,
"server_metadata_url": OPENID_PROVIDER_URL.value,
"scope": OAUTH_SCOPES.value,
"name": OAUTH_PROVIDER_NAME.value,
"redirect_uri": OPENID_REDIRECT_URI.value,
"register": oidc_oauth_register,
}
@@ -545,7 +654,7 @@ if CUSTOM_NAME:
# STORAGE PROVIDER
####################################
STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "") # defaults to local, s3
STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "local") # 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)
@@ -553,6 +662,11 @@ 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)
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
"GOOGLE_APPLICATION_CREDENTIALS_JSON", None
)
####################################
# File Upload DIR
####################################
@@ -686,6 +800,12 @@ OPENAI_API_BASE_URL = "https://api.openai.com/v1"
# WEBUI
####################################
WEBUI_URL = PersistentConfig(
"WEBUI_URL", "webui.url", os.environ.get("WEBUI_URL", "http://localhost:3000")
)
ENABLE_SIGNUP = PersistentConfig(
"ENABLE_SIGNUP",
"ui.enable_signup",
@@ -702,6 +822,7 @@ ENABLE_LOGIN_FORM = PersistentConfig(
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
)
DEFAULT_LOCALE = PersistentConfig(
"DEFAULT_LOCALE",
"ui.default_locale",
@@ -758,7 +879,6 @@ DEFAULT_USER_ROLE = PersistentConfig(
os.getenv("DEFAULT_USER_ROLE", "pending"),
)
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
== "true"
@@ -778,6 +898,10 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
)
USER_PERMISSIONS_CHAT_CONTROLS = (
os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
)
@@ -794,23 +918,45 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
)
USER_PERMISSIONS_FEATURES_WEB_SEARCH = (
os.environ.get("USER_PERMISSIONS_FEATURES_WEB_SEARCH", "True").lower() == "true"
)
USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = (
os.environ.get("USER_PERMISSIONS_FEATURES_IMAGE_GENERATION", "True").lower()
== "true"
)
DEFAULT_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": {
"controls": USER_PERMISSIONS_CHAT_CONTROLS,
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
},
"features": {
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
"image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
},
}
USER_PERMISSIONS = PersistentConfig(
"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": {
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
},
},
DEFAULT_USER_PERMISSIONS,
)
ENABLE_CHANNELS = PersistentConfig(
"ENABLE_CHANNELS",
"channels.enable",
os.environ.get("ENABLE_CHANNELS", "False").lower() == "true",
)
@@ -948,12 +1094,71 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
)
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """Create a concise, 3-5 word title with an emoji as a title for the chat history, in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
Examples of titles:
📉 Stock Market Trends
🍪 Perfect Chocolate Chip Recipe
Evolution of Music Streaming
Remote Work Productivity Tips
Artificial Intelligence in Healthcare
🎮 Video Game Development Insights
<chat_history>
{{MESSAGES:END:2}}
</chat_history>"""
TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"TAGS_GENERATION_PROMPT_TEMPLATE",
"task.tags.prompt_template",
os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""),
)
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task:
Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
### Guidelines:
- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
- If content is too short (less than 3 messages) or too diverse, use only ["General"]
- Use the chat's primary language; default to English if multilingual
- Prioritize accuracy over specificity
### Output:
JSON format: { "tags": ["tag1", "tag2", "tag3"] }
### Chat History:
<chat_history>
{{MESSAGES:END:6}}
</chat_history>"""
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE",
"task.image.prompt_template",
os.environ.get("IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE", ""),
)
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task:
Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements.
### Guidelines:
- Be descriptive and detailed, focusing on the most important aspects of the image.
- Avoid making assumptions or adding information not present in the image.
- Use the chat's primary language; default to English if multilingual.
- If the image is too complex, focus on the most prominent elements.
### Output:
Strictly return in JSON format:
{
"prompt": "Your detailed description here."
}
### Chat History:
<chat_history>
{{MESSAGES:END:6}}
</chat_history>"""
ENABLE_TAGS_GENERATION = PersistentConfig(
"ENABLE_TAGS_GENERATION",
"task.tags.enable",
@@ -1072,6 +1277,19 @@ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
)
DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text."""
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
Message: ```{{prompt}}```"""
DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.
Responses from models: {{responses}}"""
####################################
# Vector Database
####################################
@@ -1100,6 +1318,7 @@ CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# Milvus
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
MILVUS_DB = os.environ.get("MILVUS_DB", "default")
# Qdrant
QDRANT_URI = os.environ.get("QDRANT_URI", None)
@@ -1118,11 +1337,34 @@ 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."
)
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(
os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
)
####################################
# Information Retrieval (RAG)
####################################
# If configured, Google Drive will be available as an upload option.
ENABLE_GOOGLE_DRIVE_INTEGRATION = PersistentConfig(
"ENABLE_GOOGLE_DRIVE_INTEGRATION",
"google_drive.enable",
os.getenv("ENABLE_GOOGLE_DRIVE_INTEGRATION", "False").lower() == "true",
)
GOOGLE_DRIVE_CLIENT_ID = PersistentConfig(
"GOOGLE_DRIVE_CLIENT_ID",
"google_drive.client_id",
os.environ.get("GOOGLE_DRIVE_CLIENT_ID", ""),
)
GOOGLE_DRIVE_API_KEY = PersistentConfig(
"GOOGLE_DRIVE_API_KEY",
"google_drive.api_key",
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
)
# RAG Content Extraction
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
@@ -1197,7 +1439,8 @@ 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", "True").lower() == "true"
not OFFLINE_MODE
and os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "True").lower() == "true"
)
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
@@ -1222,7 +1465,8 @@ 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", "True").lower() == "true"
not OFFLINE_MODE
and os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "True").lower() == "true"
)
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
@@ -1356,6 +1600,7 @@ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
],
)
SEARXNG_QUERY_URL = PersistentConfig(
"SEARXNG_QUERY_URL",
"rag.web.search.searxng_query_url",
@@ -1380,6 +1625,12 @@ BRAVE_SEARCH_API_KEY = PersistentConfig(
os.getenv("BRAVE_SEARCH_API_KEY", ""),
)
KAGI_SEARCH_API_KEY = PersistentConfig(
"KAGI_SEARCH_API_KEY",
"rag.web.search.kagi_search_api_key",
os.getenv("KAGI_SEARCH_API_KEY", ""),
)
MOJEEK_SEARCH_API_KEY = PersistentConfig(
"MOJEEK_SEARCH_API_KEY",
"rag.web.search.mojeek_search_api_key",
@@ -1477,6 +1728,13 @@ ENABLE_IMAGE_GENERATION = PersistentConfig(
"image_generation.enable",
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
)
ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
"ENABLE_IMAGE_PROMPT_GENERATION",
"image_generation.prompt.enable",
os.environ.get("ENABLE_IMAGE_PROMPT_GENERATION", "true").lower() == "true",
)
AUTOMATIC1111_BASE_URL = PersistentConfig(
"AUTOMATIC1111_BASE_URL",
"image_generation.automatic1111.base_url",
@@ -1525,6 +1783,12 @@ COMFYUI_BASE_URL = PersistentConfig(
os.getenv("COMFYUI_BASE_URL", ""),
)
COMFYUI_API_KEY = PersistentConfig(
"COMFYUI_API_KEY",
"image_generation.comfyui.api_key",
os.getenv("COMFYUI_API_KEY", ""),
)
COMFYUI_DEFAULT_WORKFLOW = """
{
"3": {
@@ -1686,7 +1950,8 @@ WHISPER_MODEL = PersistentConfig(
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"
not OFFLINE_MODE
and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
)
@@ -1799,6 +2064,12 @@ LDAP_SERVER_PORT = PersistentConfig(
int(os.environ.get("LDAP_SERVER_PORT", "389")),
)
LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig(
"LDAP_ATTRIBUTE_FOR_MAIL",
"ldap.server.attribute_for_mail",
os.environ.get("LDAP_ATTRIBUTE_FOR_MAIL", "mail"),
)
LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
"LDAP_ATTRIBUTE_FOR_USERNAME",
"ldap.server.attribute_for_username",
+1
View File
@@ -113,6 +113,7 @@ class TASKS(str, Enum):
TAGS_GENERATION = "tags_generation"
EMOJI_GENERATION = "emoji_generation"
QUERY_GENERATION = "query_generation"
IMAGE_PROMPT_GENERATION = "image_prompt_generation"
AUTOCOMPLETE_GENERATION = "autocomplete_generation"
FUNCTION_CALLING = "function_calling"
MOA_RESPONSE_GENERATION = "moa_response_generation"
+21 -3
View File
@@ -53,6 +53,13 @@ if USE_CUDA.lower() == "true":
else:
DEVICE_TYPE = "cpu"
try:
import torch
if torch.backends.mps.is_available() and torch.backends.mps.is_built():
DEVICE_TYPE = "mps"
except Exception:
pass
####################################
# LOGGING
@@ -103,8 +110,6 @@ WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
WEBUI_URL = os.environ.get("WEBUI_URL", "http://localhost:3000")
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
@@ -269,6 +274,8 @@ 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_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
if DATABASE_POOL_SIZE == "":
@@ -313,6 +320,11 @@ RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
ENABLE_REALTIME_CHAT_SAVE = (
os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
)
####################################
# REDIS
####################################
@@ -329,6 +341,9 @@ WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
)
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
BYPASS_MODEL_ACCESS_CONTROL = (
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
)
####################################
# WEBUI_SECRET_KEY
@@ -373,7 +388,7 @@ else:
AIOHTTP_CLIENT_TIMEOUT = 300
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "5"
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
)
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
@@ -391,3 +406,6 @@ else:
####################################
OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
if OFFLINE_MODE:
os.environ["HF_HUB_OFFLINE"] = "1"
+316
View File
@@ -0,0 +1,316 @@
import logging
import sys
import inspect
import json
from pydantic import BaseModel
from typing import AsyncGenerator, Generator, Iterator
from fastapi import (
Depends,
FastAPI,
File,
Form,
HTTPException,
Request,
UploadFile,
status,
)
from starlette.responses import Response, StreamingResponse
from open_webui.socket.main import (
get_event_call,
get_event_emitter,
)
from open_webui.models.functions import Functions
from open_webui.models.models import Models
from open_webui.utils.plugin import load_function_module_by_id
from open_webui.utils.tools import get_tools
from open_webui.utils.access_control import has_access
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
from open_webui.utils.misc import (
add_or_update_system_message,
get_last_user_message,
prepend_to_first_user_message_content,
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
)
from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_function_module_by_id(request: Request, pipe_id: str):
# Check if function is already loaded
if pipe_id not in request.app.state.FUNCTIONS:
function_module, _, _ = load_function_module_by_id(pipe_id)
request.app.state.FUNCTIONS[pipe_id] = function_module
else:
function_module = request.app.state.FUNCTIONS[pipe_id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe_id)
function_module.valves = function_module.Valves(**(valves if valves else {}))
return function_module
async def get_function_models(request):
pipes = Functions.get_functions_by_type("pipe", active_only=True)
pipe_models = []
for pipe in pipes:
function_module = get_function_module_by_id(request, pipe.id)
# Check if function is a manifold
if hasattr(function_module, "pipes"):
sub_pipes = []
# Check if pipes is a function or a list
try:
if callable(function_module.pipes):
sub_pipes = function_module.pipes()
else:
sub_pipes = function_module.pipes
except Exception as e:
log.exception(e)
sub_pipes = []
log.debug(
f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
)
for p in sub_pipes:
sub_pipe_id = f'{pipe.id}.{p["id"]}'
sub_pipe_name = p["name"]
if hasattr(function_module, "name"):
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
pipe_flag = {"type": pipe.type}
pipe_models.append(
{
"id": sub_pipe_id,
"name": sub_pipe_name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
else:
pipe_flag = {"type": "pipe"}
log.debug(
f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
)
pipe_models.append(
{
"id": pipe.id,
"name": pipe.name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
return pipe_models
async def generate_function_chat_completion(
request, form_data, user, models: dict = {}
):
async def execute_pipe(pipe, params):
if inspect.iscoroutinefunction(pipe):
return await pipe(**params)
else:
return pipe(**params)
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
if isinstance(res, str):
return res
if isinstance(res, Generator):
return "".join(map(str, res))
if isinstance(res, AsyncGenerator):
return "".join([str(stream) async for stream in res])
def process_line(form_data: dict, line):
if isinstance(line, BaseModel):
line = line.model_dump_json()
line = f"data: {line}"
if isinstance(line, dict):
line = f"data: {json.dumps(line)}"
try:
line = line.decode("utf-8")
except Exception:
pass
if line.startswith("data:"):
return f"{line}\n\n"
else:
line = openai_chat_chunk_message_template(form_data["model"], line)
return f"data: {json.dumps(line)}\n\n"
def get_pipe_id(form_data: dict) -> str:
pipe_id = form_data["model"]
if "." in pipe_id:
pipe_id, _ = pipe_id.split(".", 1)
return pipe_id
def get_function_params(function_module, form_data, user, extra_params=None):
if extra_params is None:
extra_params = {}
pipe_id = get_pipe_id(form_data)
# Get the signature of the function
sig = inspect.signature(function_module.pipe)
params = {"body": form_data} | {
k: v for k, v in extra_params.items() if k in sig.parameters
}
if "__user__" in params and hasattr(function_module, "UserValves"):
user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
try:
params["__user__"]["valves"] = function_module.UserValves(**user_valves)
except Exception as e:
log.exception(e)
params["__user__"]["valves"] = function_module.UserValves()
return params
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
metadata = form_data.pop("metadata", {})
files = metadata.get("files", [])
tool_ids = metadata.get("tool_ids", [])
# Check if tool_ids is None
if tool_ids is None:
tool_ids = []
__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,
"email": user.email,
"name": user.name,
"role": user.role,
},
"__metadata__": metadata,
"__request__": request,
}
extra_params["__tools__"] = get_tools(
request,
tool_ids,
user,
{
**extra_params,
"__model__": models.get(form_data["model"], None),
"__messages__": form_data["messages"],
"__files__": files,
},
)
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
params = model_info.params.model_dump()
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(params, form_data, user)
pipe_id = get_pipe_id(form_data)
function_module = get_function_module_by_id(request, pipe_id)
pipe = function_module.pipe
params = get_function_params(function_module, form_data, user, extra_params)
if form_data.get("stream", False):
async def stream_content():
try:
res = await execute_pipe(pipe, params)
# Directly return if the response is a StreamingResponse
if isinstance(res, StreamingResponse):
async for data in res.body_iterator:
yield data
return
if isinstance(res, dict):
yield f"data: {json.dumps(res)}\n\n"
return
except Exception as e:
log.error(f"Error: {e}")
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
return
if isinstance(res, str):
message = openai_chat_chunk_message_template(form_data["model"], res)
yield f"data: {json.dumps(message)}\n\n"
if isinstance(res, Iterator):
for line in res:
yield process_line(form_data, line)
if isinstance(res, AsyncGenerator):
async for line in res:
yield process_line(form_data, line)
if isinstance(res, str) or isinstance(res, Generator):
finish_message = openai_chat_chunk_message_template(
form_data["model"], ""
)
finish_message["choices"][0]["finish_reason"] = "stop"
yield f"data: {json.dumps(finish_message)}\n\n"
yield "data: [DONE]"
return StreamingResponse(stream_content(), media_type="text/event-stream")
else:
try:
res = await execute_pipe(pipe, params)
except Exception as e:
log.error(f"Error: {e}")
return {"error": {"detail": str(e)}}
if isinstance(res, StreamingResponse) or isinstance(res, dict):
return res
if isinstance(res, BaseModel):
return res.model_dump()
message = await get_message_content(res)
return openai_chat_completion_message_template(form_data["model"], message)
@@ -3,10 +3,11 @@ import logging
from contextlib import contextmanager
from typing import Any, Optional
from open_webui.apps.webui.internal.wrappers import register_connection
from open_webui.internal.wrappers import register_connection
from open_webui.env import (
OPEN_WEBUI_DIR,
DATABASE_URL,
DATABASE_SCHEMA,
SRC_LOG_LEVELS,
DATABASE_POOL_MAX_OVERFLOW,
DATABASE_POOL_RECYCLE,
@@ -14,7 +15,7 @@ from open_webui.env import (
DATABASE_POOL_TIMEOUT,
)
from peewee_migrate import Router
from sqlalchemy import Dialect, create_engine, types
from sqlalchemy import Dialect, create_engine, MetaData, types
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.pool import QueuePool, NullPool
@@ -54,7 +55,7 @@ def handle_peewee_migration(DATABASE_URL):
try:
# Replace the postgresql:// with postgres:// to handle the peewee migration
db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://"))
migrate_dir = OPEN_WEBUI_DIR / "apps" / "webui" / "internal" / "migrations"
migrate_dir = OPEN_WEBUI_DIR / "internal" / "migrations"
router = Router(db, logger=log, migrate_dir=migrate_dir)
router.run()
db.close()
@@ -99,7 +100,8 @@ else:
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
Base = declarative_base()
metadata_obj = MetaData(schema=DATABASE_SCHEMA)
Base = declarative_base(metadata=metadata_obj)
Session = scoped_session(SessionLocal)
+720 -2320
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
from logging.config import fileConfig
from alembic import context
from open_webui.apps.webui.models.auths import Auth
from open_webui.models.auths import Auth
from open_webui.env import DATABASE_URL
from sqlalchemy import engine_from_config, pool
+1 -1
View File
@@ -9,7 +9,7 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.apps.webui.internal.db
import open_webui.internal.db
${imports if imports else ""}
# revision identifiers, used by Alembic.
@@ -0,0 +1,70 @@
"""Update message & channel tables
Revision ID: 3781e22d8b01
Revises: 7826ab40b532
Create Date: 2024-12-30 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "3781e22d8b01"
down_revision = "7826ab40b532"
branch_labels = None
depends_on = None
def upgrade():
# Add 'type' column to the 'channel' table
op.add_column(
"channel",
sa.Column(
"type",
sa.Text(),
nullable=True,
),
)
# Add 'parent_id' column to the 'message' table for threads
op.add_column(
"message",
sa.Column("parent_id", sa.Text(), nullable=True),
)
op.create_table(
"message_reaction",
sa.Column(
"id", sa.Text(), nullable=False, primary_key=True, unique=True
), # Unique reaction ID
sa.Column("user_id", sa.Text(), nullable=False), # User who reacted
sa.Column(
"message_id", sa.Text(), nullable=False
), # Message that was reacted to
sa.Column(
"name", sa.Text(), nullable=False
), # Reaction name (e.g. "thumbs_up")
sa.Column(
"created_at", sa.BigInteger(), nullable=True
), # Timestamp of when the reaction was added
)
op.create_table(
"channel_member",
sa.Column(
"id", sa.Text(), nullable=False, primary_key=True, unique=True
), # Record ID for the membership row
sa.Column("channel_id", sa.Text(), nullable=False), # Associated channel
sa.Column("user_id", sa.Text(), nullable=False), # Associated user
sa.Column(
"created_at", sa.BigInteger(), nullable=True
), # Timestamp of when the user joined the channel
)
def downgrade():
# Revert 'type' column addition to the 'channel' table
op.drop_column("channel", "type")
op.drop_column("message", "parent_id")
op.drop_table("message_reaction")
op.drop_table("channel_member")
@@ -0,0 +1,48 @@
"""Add channel table
Revision ID: 57c599a3cb57
Revises: 922e7a387820
Create Date: 2024-12-22 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "57c599a3cb57"
down_revision = "922e7a387820"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"channel",
sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column("user_id", sa.Text()),
sa.Column("name", sa.Text()),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("access_control", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
)
op.create_table(
"message",
sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column("user_id", sa.Text()),
sa.Column("channel_id", sa.Text(), nullable=True),
sa.Column("content", sa.Text()),
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
)
def downgrade():
op.drop_table("channel")
op.drop_table("message")
@@ -0,0 +1,26 @@
"""Update file table
Revision ID: 7826ab40b532
Revises: 57c599a3cb57
Create Date: 2024-12-23 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "7826ab40b532"
down_revision = "57c599a3cb57"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"file",
sa.Column("access_control", sa.JSON(), nullable=True),
)
def downgrade():
op.drop_column("file", "access_control")
@@ -11,8 +11,8 @@ from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
import open_webui.apps.webui.internal.db
from open_webui.apps.webui.internal.db import JSONField
import open_webui.internal.db
from open_webui.internal.db import JSONField
from open_webui.migrations.util import get_existing_tables
# revision identifiers, used by Alembic.
@@ -2,12 +2,12 @@ import logging
import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.apps.webui.models.users import UserModel, Users
from open_webui.internal.db import Base, get_db
from open_webui.models.users import UserModel, Users
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel
from sqlalchemy import Boolean, Column, String, Text
from open_webui.utils.utils import verify_password
from open_webui.utils.auth import verify_password
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
+136
View File
@@ -0,0 +1,136 @@
import json
import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.utils.access_control import has_access
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from sqlalchemy import or_, func, select, and_, text
from sqlalchemy.sql import exists
####################
# Channel DB Schema
####################
class Channel(Base):
__tablename__ = "channel"
id = Column(Text, primary_key=True)
user_id = Column(Text)
type = Column(Text, nullable=True)
name = Column(Text)
description = Column(Text, nullable=True)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class ChannelModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
type: Optional[str] = None
name: str
description: Optional[str] = None
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 ChannelForm(BaseModel):
name: str
description: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
class ChannelTable:
def insert_new_channel(
self, type: Optional[str], form_data: ChannelForm, user_id: str
) -> Optional[ChannelModel]:
with get_db() as db:
channel = ChannelModel(
**{
**form_data.model_dump(),
"type": type,
"name": form_data.name.lower(),
"id": str(uuid.uuid4()),
"user_id": user_id,
"created_at": int(time.time_ns()),
"updated_at": int(time.time_ns()),
}
)
new_channel = Channel(**channel.model_dump())
db.add(new_channel)
db.commit()
return channel
def get_channels(self) -> list[ChannelModel]:
with get_db() as db:
channels = db.query(Channel).all()
return [ChannelModel.model_validate(channel) for channel in channels]
def get_channels_by_user_id(
self, user_id: str, permission: str = "read"
) -> list[ChannelModel]:
channels = self.get_channels()
return [
channel
for channel in channels
if channel.user_id == user_id
or has_access(user_id, permission, channel.access_control)
]
def get_channel_by_id(self, id: str) -> Optional[ChannelModel]:
with get_db() as db:
channel = db.query(Channel).filter(Channel.id == id).first()
return ChannelModel.model_validate(channel) if channel else None
def update_channel_by_id(
self, id: str, form_data: ChannelForm
) -> Optional[ChannelModel]:
with get_db() as db:
channel = db.query(Channel).filter(Channel.id == id).first()
if not channel:
return None
channel.name = form_data.name
channel.data = form_data.data
channel.meta = form_data.meta
channel.access_control = form_data.access_control
channel.updated_at = int(time.time_ns())
db.commit()
return ChannelModel.model_validate(channel) if channel else None
def delete_channel_by_id(self, id: str):
with get_db() as db:
db.query(Channel).filter(Channel.id == id).delete()
db.commit()
return True
Channels = ChannelTable()
@@ -3,8 +3,8 @@ 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.tags import TagModel, Tag, Tags
from open_webui.internal.db import Base, get_db
from open_webui.models.tags import TagModel, Tag, Tags
from pydantic import BaseModel, ConfigDict
@@ -168,6 +168,100 @@ class ChatTable:
except Exception:
return None
def update_chat_title_by_id(self, id: str, title: str) -> Optional[ChatModel]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
chat = chat.chat
chat["title"] = title
return self.update_chat_by_id(id, chat)
def update_chat_tags_by_id(
self, id: str, tags: list[str], user
) -> Optional[ChatModel]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
self.delete_all_tags_by_id_and_user_id(id, user.id)
for tag in chat.meta.get("tags", []):
if self.count_chats_by_tag_name_and_user_id(tag, user.id) == 0:
Tags.delete_tag_by_name_and_user_id(tag, user.id)
for tag_name in tags:
if tag_name.lower() == "none":
continue
self.add_chat_tag_by_id_and_user_id_and_tag_name(id, user.id, tag_name)
return self.get_chat_by_id(id)
def get_chat_title_by_id(self, id: str) -> Optional[str]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
return chat.chat.get("title", "New Chat")
def get_messages_by_chat_id(self, id: str) -> Optional[dict]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
return chat.chat.get("history", {}).get("messages", {}) or {}
def get_message_by_id_and_message_id(
self, id: str, message_id: str
) -> Optional[dict]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
return chat.chat.get("history", {}).get("messages", {}).get(message_id, {})
def upsert_message_to_chat_by_id_and_message_id(
self, id: str, message_id: str, message: dict
) -> Optional[ChatModel]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
chat = chat.chat
history = chat.get("history", {})
if message_id in history.get("messages", {}):
history["messages"][message_id] = {
**history["messages"][message_id],
**message,
}
else:
history["messages"][message_id] = message
history["currentId"] = message_id
chat["history"] = history
return self.update_chat_by_id(id, chat)
def add_message_status_to_chat_by_id_and_message_id(
self, id: str, message_id: str, status: dict
) -> Optional[ChatModel]:
chat = self.get_chat_by_id(id)
if chat is None:
return None
chat = chat.chat
history = chat.get("history", {})
if message_id in history.get("messages", {}):
status_history = history["messages"][message_id].get("statusHistory", [])
status_history.append(status)
history["messages"][message_id]["statusHistory"] = status_history
chat["history"] = history
return self.update_chat_by_id(id, chat)
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
with get_db() as db:
# Get the existing chat to share
@@ -299,7 +393,7 @@ class ChatTable:
limit: int = 50,
) -> list[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
query = db.query(Chat).filter_by(user_id=user_id)
if not include_archived:
query = query.filter_by(archived=False)
@@ -375,6 +469,8 @@ class ChatTable:
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
# it is possible that the shared link was deleted. hence,
# we check if the chat is still shared by checkng if a chat with the share_id exists
chat = db.query(Chat).filter_by(share_id=id).first()
if chat:
@@ -3,8 +3,8 @@ 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.internal.db import Base, get_db
from open_webui.models.chats import Chats
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
@@ -2,7 +2,7 @@ import logging
import time
from typing import Optional
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
from open_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, JSON
@@ -27,6 +27,8 @@ class File(Base):
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
@@ -44,6 +46,8 @@ class FileModel(BaseModel):
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
created_at: Optional[int] # timestamp in epoch
updated_at: Optional[int] # timestamp in epoch
@@ -90,6 +94,7 @@ class FileForm(BaseModel):
path: str
data: dict = {}
meta: dict = {}
access_control: Optional[dict] = None
class FilesTable:
@@ -3,8 +3,8 @@ 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.internal.db import Base, get_db
from open_webui.models.chats import Chats
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
@@ -2,8 +2,8 @@ import logging
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.internal.db import Base, JSONField, get_db
from open_webui.models.users import Users
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text
@@ -4,10 +4,10 @@ import time
from typing import Optional
import uuid
from open_webui.apps.webui.internal.db import Base, get_db
from open_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.models.files import FileMetadataResponse
from pydantic import BaseModel, ConfigDict
@@ -80,12 +80,11 @@ class GroupResponse(BaseModel):
class GroupForm(BaseModel):
name: str
description: str
permissions: Optional[dict] = None
class GroupUpdateForm(GroupForm):
permissions: Optional[dict] = None
user_ids: Optional[list[str]] = None
admin_ids: Optional[list[str]] = None
class GroupTable:
@@ -95,7 +94,7 @@ class GroupTable:
with get_db() as db:
group = GroupModel(
**{
**form_data.model_dump(),
**form_data.model_dump(exclude_none=True),
"id": str(uuid.uuid4()),
"user_id": user_id,
"created_at": int(time.time()),
@@ -146,6 +145,13 @@ class GroupTable:
except Exception:
return None
def get_group_user_ids_by_id(self, id: str) -> Optional[str]:
group = self.get_group_by_id(id)
if group:
return group.user_ids
else:
return None
def update_group_by_id(
self, id: str, form_data: GroupUpdateForm, overwrite: bool = False
) -> Optional[GroupModel]:
@@ -182,5 +188,24 @@ class GroupTable:
except Exception:
return False
def remove_user_from_all_groups(self, user_id: str) -> bool:
with get_db() as db:
try:
groups = self.get_groups_by_member_id(user_id)
for group in groups:
group.user_ids.remove(user_id)
db.query(Group).filter_by(id=group.id).update(
{
"user_ids": group.user_ids,
"updated_at": int(time.time()),
}
)
db.commit()
return True
except Exception:
return False
Groups = GroupTable()
@@ -4,11 +4,11 @@ import time
from typing import Optional
import uuid
from open_webui.apps.webui.internal.db import Base, get_db
from open_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 open_webui.models.files import FileMetadataResponse
from open_webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
@@ -2,7 +2,7 @@ import time
import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.internal.db import Base, get_db
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
+279
View File
@@ -0,0 +1,279 @@
import json
import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.models.tags import TagModel, Tag, Tags
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from sqlalchemy import or_, func, select, and_, text
from sqlalchemy.sql import exists
####################
# Message DB Schema
####################
class MessageReaction(Base):
__tablename__ = "message_reaction"
id = Column(Text, primary_key=True)
user_id = Column(Text)
message_id = Column(Text)
name = Column(Text)
created_at = Column(BigInteger)
class MessageReactionModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
message_id: str
name: str
created_at: int # timestamp in epoch
class Message(Base):
__tablename__ = "message"
id = Column(Text, primary_key=True)
user_id = Column(Text)
channel_id = Column(Text, nullable=True)
parent_id = Column(Text, nullable=True)
content = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
created_at = Column(BigInteger) # time_ns
updated_at = Column(BigInteger) # time_ns
class MessageModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
channel_id: Optional[str] = None
parent_id: Optional[str] = None
content: str
data: Optional[dict] = None
meta: Optional[dict] = None
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
####################
# Forms
####################
class MessageForm(BaseModel):
content: str
parent_id: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
class Reactions(BaseModel):
name: str
user_ids: list[str]
count: int
class MessageResponse(MessageModel):
latest_reply_at: Optional[int]
reply_count: int
reactions: list[Reactions]
class MessageTable:
def insert_new_message(
self, form_data: MessageForm, channel_id: str, user_id: str
) -> Optional[MessageModel]:
with get_db() as db:
id = str(uuid.uuid4())
ts = int(time.time_ns())
message = MessageModel(
**{
"id": id,
"user_id": user_id,
"channel_id": channel_id,
"parent_id": form_data.parent_id,
"content": form_data.content,
"data": form_data.data,
"meta": form_data.meta,
"created_at": ts,
"updated_at": ts,
}
)
result = Message(**message.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return MessageModel.model_validate(result) if result else None
def get_message_by_id(self, id: str) -> Optional[MessageResponse]:
with get_db() as db:
message = db.get(Message, id)
if not message:
return None
reactions = self.get_reactions_by_message_id(id)
replies = self.get_replies_by_message_id(id)
return MessageResponse(
**{
**MessageModel.model_validate(message).model_dump(),
"latest_reply_at": replies[0].created_at if replies else None,
"reply_count": len(replies),
"reactions": reactions,
}
)
def get_replies_by_message_id(self, id: str) -> list[MessageModel]:
with get_db() as db:
all_messages = (
db.query(Message)
.filter_by(parent_id=id)
.order_by(Message.created_at.desc())
.all()
)
return [MessageModel.model_validate(message) for message in all_messages]
def get_reply_user_ids_by_message_id(self, id: str) -> list[str]:
with get_db() as db:
return [
message.user_id
for message in db.query(Message).filter_by(parent_id=id).all()
]
def get_messages_by_channel_id(
self, channel_id: str, skip: int = 0, limit: int = 50
) -> list[MessageModel]:
with get_db() as db:
all_messages = (
db.query(Message)
.filter_by(channel_id=channel_id, parent_id=None)
.order_by(Message.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return [MessageModel.model_validate(message) for message in all_messages]
def get_messages_by_parent_id(
self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50
) -> list[MessageModel]:
with get_db() as db:
message = db.get(Message, parent_id)
if not message:
return []
all_messages = (
db.query(Message)
.filter_by(channel_id=channel_id, parent_id=parent_id)
.order_by(Message.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
# If length of all_messages is less than limit, then add the parent message
if len(all_messages) < limit:
all_messages.append(message)
return [MessageModel.model_validate(message) for message in all_messages]
def update_message_by_id(
self, id: str, form_data: MessageForm
) -> Optional[MessageModel]:
with get_db() as db:
message = db.get(Message, id)
message.content = form_data.content
message.data = form_data.data
message.meta = form_data.meta
message.updated_at = int(time.time_ns())
db.commit()
db.refresh(message)
return MessageModel.model_validate(message) if message else None
def add_reaction_to_message(
self, id: str, user_id: str, name: str
) -> Optional[MessageReactionModel]:
with get_db() as db:
reaction_id = str(uuid.uuid4())
reaction = MessageReactionModel(
id=reaction_id,
user_id=user_id,
message_id=id,
name=name,
created_at=int(time.time_ns()),
)
result = MessageReaction(**reaction.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return MessageReactionModel.model_validate(result) if result else None
def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
with get_db() as db:
all_reactions = db.query(MessageReaction).filter_by(message_id=id).all()
reactions = {}
for reaction in all_reactions:
if reaction.name not in reactions:
reactions[reaction.name] = {
"name": reaction.name,
"user_ids": [],
"count": 0,
}
reactions[reaction.name]["user_ids"].append(reaction.user_id)
reactions[reaction.name]["count"] += 1
return [Reactions(**reaction) for reaction in reactions.values()]
def remove_reaction_by_id_and_user_id_and_name(
self, id: str, user_id: str, name: str
) -> bool:
with get_db() as db:
db.query(MessageReaction).filter_by(
message_id=id, user_id=user_id, name=name
).delete()
db.commit()
return True
def delete_reactions_by_id(self, id: str) -> bool:
with get_db() as db:
db.query(MessageReaction).filter_by(message_id=id).delete()
db.commit()
return True
def delete_replies_by_id(self, id: str) -> bool:
with get_db() as db:
db.query(Message).filter_by(parent_id=id).delete()
db.commit()
return True
def delete_message_by_id(self, id: str) -> bool:
with get_db() as db:
db.query(Message).filter_by(id=id).delete()
# Delete all reactions to this message
db.query(MessageReaction).filter_by(message_id=id).delete()
db.commit()
return True
Messages = MessageTable()
@@ -2,10 +2,10 @@ import logging
import time
from typing import Optional
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
from open_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 open_webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
@@ -1,8 +1,8 @@
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 open_webui.internal.db import Base, get_db
from open_webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON
@@ -3,7 +3,7 @@ import time
import uuid
from typing import Optional
from open_webui.apps.webui.internal.db import Base, get_db
from open_webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
@@ -2,8 +2,8 @@ import logging
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, UserResponse
from open_webui.internal.db import Base, JSONField, get_db
from open_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, JSON
@@ -1,8 +1,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.chats import Chats
from open_webui.internal.db import Base, JSONField, get_db
from open_webui.models.chats import Chats
from open_webui.models.groups import Groups
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
@@ -70,6 +75,13 @@ class UserResponse(BaseModel):
profile_image_url: str
class UserNameResponse(BaseModel):
id: str
name: str
role: str
profile_image_url: str
class UserRoleUpdateForm(BaseModel):
id: str
role: str
@@ -147,13 +159,25 @@ class UsersTable:
except Exception:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> list[UserModel]:
def get_users(
self, skip: Optional[int] = None, limit: Optional[int] = None
) -> list[UserModel]:
with get_db() as db:
users = (
db.query(User)
# .offset(skip).limit(limit)
.all()
)
query = db.query(User).order_by(User.created_at.desc())
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
users = query.all()
return [UserModel.model_validate(user) for user in users]
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
with get_db() as db:
users = db.query(User).filter(User.id.in_(user_ids)).all()
return [UserModel.model_validate(user) for user in users]
def get_num_users(self) -> Optional[int]:
@@ -168,6 +192,22 @@ class UsersTable:
except Exception:
return None
def get_user_webhook_url_by_id(self, id: str) -> Optional[str]:
try:
with get_db() as db:
user = db.query(User).filter_by(id=id).first()
if user.settings is None:
return None
else:
return (
user.settings.get("ui", {})
.get("notifications", {})
.get("webhook_url", None)
)
except Exception:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try:
with get_db() as db:
@@ -233,9 +273,11 @@ class UsersTable:
def delete_user_by_id(self, id: str) -> bool:
try:
# Remove User from Groups
Groups.remove_user_from_all_groups(id)
# Delete User Chats
result = Chats.delete_chats_by_user_id(id)
if result:
with get_db() as db:
# Delete User
@@ -265,5 +307,10 @@ class UsersTable:
except Exception:
return None
def get_valid_user_ids(self, user_ids: list[str]) -> list[str]:
with get_db() as db:
users = db.query(User).filter(User.id.in_(user_ids)).all()
return [user.id for user in users]
Users = UsersTable()
@@ -1,6 +1,7 @@
import requests
import logging
import ftfy
import sys
from langchain_community.document_loaders import (
BSHTMLLoader,
@@ -18,8 +19,9 @@ from langchain_community.document_loaders import (
YoutubeLoader,
)
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -106,7 +108,7 @@ class TikaLoader:
if "Content-Type" in raw_metadata:
headers["Content-Type"] = raw_metadata["Content-Type"]
log.info("Tika extracted text: %s", text)
log.debug("Tika extracted text: %s", text)
return [Document(page_content=text, metadata=headers)]
else:
@@ -11,10 +11,12 @@ from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriev
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.misc import get_last_user_message
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import SRC_LOG_LEVELS, OFFLINE_MODE
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -70,7 +72,9 @@ def query_doc(
limit=k,
)
log.info(f"query_doc:result {result.ids} {result.metadatas}")
if result:
log.info(f"query_doc:result {result.ids} {result.metadatas}")
return result
except Exception as e:
print(e)
@@ -197,7 +201,12 @@ def query_collection(
else:
pass
return merge_and_sort_query_results(results, k=k)
if VECTOR_DB == "chroma":
# Chroma uses unconventional cosine similarity, so we don't need to reverse the results
# https://docs.trychroma.com/docs/collections/configure#configuring-chroma-collections
return merge_and_sort_query_results(results, k=k, reverse=False)
else:
return merge_and_sort_query_results(results, k=k, reverse=True)
def query_collection_with_hybrid_search(
@@ -233,7 +242,12 @@ def query_collection_with_hybrid_search(
"Hybrid search failed for all collections. Using Non hybrid search as fallback."
)
return merge_and_sort_query_results(results, k=k, reverse=True)
if VECTOR_DB == "chroma":
# Chroma uses unconventional cosine similarity, so we don't need to reverse the results
# https://docs.trychroma.com/docs/collections/configure#configuring-chroma-collections
return merge_and_sort_query_results(results, k=k, reverse=False)
else:
return merge_and_sort_query_results(results, k=k, reverse=True)
def get_embedding_function(
@@ -373,6 +387,9 @@ def get_model_path(model: str, update_model: bool = False):
local_files_only = not update_model
if OFFLINE_MODE:
local_files_only = True
snapshot_kwargs = {
"cache_dir": cache_dir,
"local_files_only": local_files_only,
@@ -0,0 +1,22 @@
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
VECTOR_DB_CLIENT = MilvusClient()
elif VECTOR_DB == "qdrant":
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
VECTOR_DB_CLIENT = QdrantClient()
elif VECTOR_DB == "opensearch":
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
VECTOR_DB_CLIENT = OpenSearchClient()
elif VECTOR_DB == "pgvector":
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
else:
from open_webui.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.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
CHROMA_DATA_PATH,
CHROMA_HTTP_HOST,
@@ -51,8 +51,8 @@ class ChromaClient:
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
collections = self.client.list_collections()
return collection_name in [collection.name for collection in collections]
collection_names = self.client.list_collections()
return collection_name in collection_names
def delete_collection(self, collection_name: str):
# Delete the collection based on the collection name.
@@ -4,16 +4,17 @@ import json
from typing import Optional
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
MILVUS_URI,
MILVUS_DB,
)
class MilvusClient:
def __init__(self):
self.collection_prefix = "open_webui"
self.client = Client(uri=MILVUS_URI)
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
def _result_to_get_result(self, result) -> GetResult:
ids = []
@@ -1,7 +1,7 @@
from opensearchpy import OpenSearch
from typing import Optional
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
OPENSEARCH_URI,
OPENSEARCH_SSL,
@@ -5,9 +5,11 @@ from sqlalchemy import (
create_engine,
Column,
Integer,
MetaData,
select,
text,
Text,
Table,
values,
)
from sqlalchemy.sql import true
@@ -17,11 +19,12 @@ 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 sqlalchemy.exc import NoSuchTableError
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import PGVECTOR_DB_URL
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
VECTOR_LENGTH = 1536
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
Base = declarative_base()
@@ -40,7 +43,7 @@ class PgvectorClient:
# if no pgvector uri, use the existing database connection
if not PGVECTOR_DB_URL:
from open_webui.apps.webui.internal.db import Session
from open_webui.internal.db import Session
self.session = Session
else:
@@ -56,6 +59,9 @@ class PgvectorClient:
# Ensure the pgvector extension is available
self.session.execute(text("CREATE EXTENSION IF NOT EXISTS vector;"))
# Check vector length consistency
self.check_vector_length()
# Create the tables if they do not exist
# Base.metadata.create_all requires a bind (engine or connection)
# Get the connection from the session
@@ -82,6 +88,41 @@ class PgvectorClient:
print(f"Error during initialization: {e}")
raise
def check_vector_length(self) -> None:
"""
Check if the VECTOR_LENGTH matches the existing vector column dimension in the database.
Raises an exception if there is a mismatch.
"""
metadata = MetaData()
try:
# Attempt to reflect the 'document_chunk' table
document_chunk_table = Table(
"document_chunk", metadata, autoload_with=self.session.bind
)
except NoSuchTableError:
# Table does not exist; no action needed
return
# Proceed to check the vector column
if "vector" in document_chunk_table.columns:
vector_column = document_chunk_table.columns["vector"]
vector_type = vector_column.type
if isinstance(vector_type, Vector):
db_vector_length = vector_type.dim
if db_vector_length != VECTOR_LENGTH:
raise Exception(
f"VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. "
"Cannot change vector size after initialization without migrating the data."
)
else:
raise Exception(
"The 'vector' column exists but is not of type 'Vector'."
)
else:
raise Exception(
"The 'vector' column does not exist in the 'document_chunk' table."
)
def adjust_vector_length(self, vector: List[float]) -> List[float]:
# Adjust vector to have length VECTOR_LENGTH
current_length = len(vector)
@@ -4,7 +4,7 @@ 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.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
NO_LIMIT = 999999999
@@ -3,7 +3,7 @@ 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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
import argparse
@@ -23,7 +23,7 @@ def search_bing(
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
mkt = locale
params = {"q": query, "mkt": mkt, "answerCount": count}
params = {"q": query, "mkt": mkt, "count": count}
headers = {"Ocp-Apim-Subscription-Key": subscription_key}
try:
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult
from open_webui.retrieval.web.main import SearchResult
from open_webui.env import SRC_LOG_LEVELS
from yarl import URL
+48
View File
@@ -0,0 +1,48 @@
import logging
from typing import Optional
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_kagi(
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
) -> list[SearchResult]:
"""Search using Kagi's Search API and return the results as a list of SearchResult objects.
The Search API will inherit the settings in your account, including results personalization and snippet length.
Args:
api_key (str): A Kagi Search API key
query (str): The query to search for
count (int): The number of results to return
"""
url = "https://kagi.com/api/v0/search"
headers = {
"Authorization": f"Bot {api_key}",
}
params = {"q": query, "limit": count}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
search_results = json_response.get("data", [])
results = [
SearchResult(
link=result["url"], title=result["title"], snippet=result.get("snippet")
)
for result in search_results
if result["t"] == 0
]
print(results)
if filter_list:
results = get_filtered_results(results, filter_list)
return results
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.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.retrieval.web.main import SearchResult
from open_webui.retrieval.web.main import SearchResult
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -683,7 +683,7 @@
"age": "October 29, 2022",
"extra_snippets": [
"You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or useable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or usable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
"To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
"Copyright © 2001-2024 Python Software Foundation. All rights reserved."
]
@@ -82,15 +82,15 @@ class SafeWebBaseLoader(WebBaseLoader):
def get_web_loader(
url: Union[str, Sequence[str]],
urls: Union[str, Sequence[str]],
verify_ssl: bool = True,
requests_per_second: int = 2,
):
# Check if the URL is valid
if not validate_url(url):
if not validate_url(urls):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return SafeWebBaseLoader(
url,
urls,
verify_ssl=verify_ssl,
requests_per_second=requests_per_second,
continue_on_failure=True,
+713
View File
@@ -0,0 +1,713 @@
import hashlib
import json
import logging
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 fastapi import (
Depends,
FastAPI,
File,
HTTPException,
Request,
UploadFile,
status,
APIRouter,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.config import (
WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR,
CACHE_DIR,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
DEVICE_TYPE,
ENABLE_FORWARD_USER_INFO_HEADERS,
)
router = APIRouter()
# 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"])
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
##########################################
#
# Utility functions
#
##########################################
from pydub import AudioSegment
from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
if not os.path.isfile(file_path):
print(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return True
return False
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav")
print(f"Converted {file_path} to {output_path}")
def set_faster_whisper_model(model: str, auto_update: bool = False):
whisper_model = None
if model:
from faster_whisper import WhisperModel
faster_whisper_kwargs = {
"model_size_or_path": model,
"device": DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu",
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not auto_update,
}
try:
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
whisper_model = WhisperModel(**faster_whisper_kwargs)
return whisper_model
##########################################
#
# Audio API
#
##########################################
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
API_KEY: str
ENGINE: str
MODEL: str
VOICE: str
SPLIT_ON: str
AZURE_SPEECH_REGION: str
AZURE_SPEECH_OUTPUT_FORMAT: str
class STTConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
WHISPER_MODEL: str
class AudioConfigUpdateForm(BaseModel):
tts: TTSConfigForm
stt: STTConfigForm
@router.get("/config")
async def get_audio_config(request: Request, user=Depends(get_admin_user)):
return {
"tts": {
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": request.app.state.config.TTS_API_KEY,
"ENGINE": request.app.state.config.TTS_ENGINE,
"MODEL": request.app.state.config.TTS_MODEL,
"VOICE": request.app.state.config.TTS_VOICE,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
"OPENAI_API_BASE_URL": request.app.state.config.STT_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.STT_OPENAI_API_KEY,
"ENGINE": request.app.state.config.STT_ENGINE,
"MODEL": request.app.state.config.STT_MODEL,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
},
}
@router.post("/config/update")
async def update_audio_config(
request: Request, form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
):
request.app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
request.app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
request.app.state.config.TTS_API_KEY = form_data.tts.API_KEY
request.app.state.config.TTS_ENGINE = form_data.tts.ENGINE
request.app.state.config.TTS_MODEL = form_data.tts.MODEL
request.app.state.config.TTS_VOICE = form_data.tts.VOICE
request.app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
request.app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
)
request.app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
request.app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
request.app.state.config.STT_ENGINE = form_data.stt.ENGINE
request.app.state.config.STT_MODEL = form_data.stt.MODEL
request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
form_data.stt.WHISPER_MODEL, WHISPER_MODEL_AUTO_UPDATE
)
return {
"tts": {
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": request.app.state.config.TTS_API_KEY,
"ENGINE": request.app.state.config.TTS_ENGINE,
"MODEL": request.app.state.config.TTS_MODEL,
"VOICE": request.app.state.config.TTS_VOICE,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
"OPENAI_API_BASE_URL": request.app.state.config.STT_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.STT_OPENAI_API_KEY,
"ENGINE": request.app.state.config.STT_ENGINE,
"MODEL": request.app.state.config.STT_MODEL,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
},
}
def load_speech_pipeline(request):
from transformers import pipeline
from datasets import load_dataset
if request.app.state.speech_synthesiser is None:
request.app.state.speech_synthesiser = pipeline(
"text-to-speech", "microsoft/speecht5_tts"
)
if request.app.state.speech_speaker_embeddings_dataset is None:
request.app.state.speech_speaker_embeddings_dataset = load_dataset(
"Matthijs/cmu-arctic-xvectors", split="validation"
)
@router.post("/speech")
async def speech(request: Request, user=Depends(get_verified_user)):
body = await request.body()
name = hashlib.sha256(
body
+ str(request.app.state.config.TTS_ENGINE).encode("utf-8")
+ str(request.app.state.config.TTS_MODEL).encode("utf-8")
).hexdigest()
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
# Check if the file already exists in the cache
if file_path.is_file():
return FileResponse(file_path)
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 request.app.state.config.TTS_ENGINE == "openai":
payload["model"] = request.app.state.config.TTS_MODEL
try:
# print(payload)
async with aiohttp.ClientSession() as session:
async with session.post(
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
json=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
) as r:
r.raise_for_status()
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(payload))
return FileResponse(file_path)
except Exception as e:
log.exception(e)
detail = None
try:
if r.status != 200:
res = await r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
detail=detail if detail else "Open WebUI: Server Connection Error",
)
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
voice_id = payload.get("voice", "")
if voice_id not in get_available_voices(request):
raise HTTPException(
status_code=400,
detail="Invalid voice id",
)
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
json={
"text": payload["input"],
"model_id": request.app.state.config.TTS_MODEL,
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
},
headers={
"Accept": "audio/mpeg",
"Content-Type": "application/json",
"xi-api-key": request.app.state.config.TTS_API_KEY,
},
) as r:
r.raise_for_status()
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(payload))
return FileResponse(file_path)
except Exception as e:
log.exception(e)
detail = None
try:
if r.status != 200:
res = await r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
detail=detail if detail else "Open WebUI: Server Connection Error",
)
elif request.app.state.config.TTS_ENGINE == "azure":
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
language = request.app.state.config.TTS_VOICE
locale = "-".join(request.app.state.config.TTS_VOICE.split("-")[:1])
output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
try:
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
<voice name="{language}">{payload["input"]}</voice>
</speak>"""
async with aiohttp.ClientSession() as session:
async with session.post(
f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1",
headers={
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY,
"Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": output_format,
},
data=data,
) as r:
r.raise_for_status()
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(payload))
return FileResponse(file_path)
except Exception as e:
log.exception(e)
detail = None
try:
if r.status != 200:
res = await r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
detail=detail if detail else "Open WebUI: Server Connection Error",
)
elif request.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")
import torch
import soundfile as sf
load_speech_pipeline(request)
embeddings_dataset = request.app.state.speech_speaker_embeddings_dataset
speaker_index = 6799
try:
speaker_index = embeddings_dataset["filename"].index(
request.app.state.config.TTS_MODEL
)
except Exception:
pass
speaker_embedding = torch.tensor(
embeddings_dataset[speaker_index]["xvector"]
).unsqueeze(0)
speech = request.app.state.speech_synthesiser(
payload["input"],
forward_params={"speaker_embeddings": speaker_embedding},
)
sf.write(file_path, speech["audio"], samplerate=speech["sampling_rate"])
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(payload))
return FileResponse(file_path)
def transcribe(request: Request, file_path):
print("transcribe", file_path)
filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
id = filename.split(".")[0]
if request.app.state.config.STT_ENGINE == "":
if request.app.state.faster_whisper_model is None:
request.app.state.faster_whisper_model = set_faster_whisper_model(
request.app.state.config.WHISPER_MODEL
)
model = request.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 request.app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
os.rename(file_path, file_path.replace(".wav", ".mp4"))
# Convert MP4 audio file to WAV format
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
r = None
try:
r = requests.post(
url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
headers={
"Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}"
},
files={"file": (filename, open(file_path, "rb"))},
data={"model": request.app.state.config.STT_MODEL},
)
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)
return data
except Exception as e:
log.exception(e)
detail = None
if r is not None:
try:
res = r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE:
file_dir = os.path.dirname(file_path)
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}")
if (
os.path.getsize(compressed_path) > MAX_FILE_SIZE
): # Still larger than MAX_FILE_SIZE after compression
raise Exception(ERROR_MESSAGES.FILE_TOO_LARGE(size=f"{MAX_FILE_SIZE_MB}MB"))
return compressed_path
else:
return file_path
@router.post("/transcriptions")
def transcription(
request: Request,
file: UploadFile = File(...),
user=Depends(get_verified_user),
):
log.info(f"file.content_type: {file.content_type}")
if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
)
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}"
with open(file_path, "wb") as f:
f.write(contents)
try:
try:
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
data = transcribe(request, file_path)
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),
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def get_available_models(request: Request) -> list[dict]:
available_models = []
if request.app.state.config.TTS_ENGINE == "openai":
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try:
response = requests.get(
"https://api.elevenlabs.io/v1/models",
headers={
"xi-api-key": request.app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
},
timeout=5,
)
response.raise_for_status()
models = response.json()
available_models = [
{"name": model["name"], "id": model["model_id"]} for model in models
]
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return available_models
@router.get("/models")
async def get_models(request: Request, user=Depends(get_verified_user)):
return {"models": get_available_models(request)}
def get_available_voices(request) -> dict:
"""Returns {voice_id: voice_name} dict"""
available_voices = {}
if request.app.state.config.TTS_ENGINE == "openai":
available_voices = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try:
available_voices = get_elevenlabs_voices(
api_key=request.app.state.config.TTS_API_KEY
)
except Exception:
# Avoided @lru_cache with exception
pass
elif request.app.state.config.TTS_ENGINE == "azure":
try:
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
headers = {
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY
}
response = requests.get(url, headers=headers)
response.raise_for_status()
voices = response.json()
for voice in voices:
available_voices[voice["ShortName"]] = (
f"{voice['DisplayName']} ({voice['ShortName']})"
)
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return available_voices
@lru_cache
def get_elevenlabs_voices(api_key: str) -> dict:
"""
Note, set the following in your .env file to use Elevenlabs:
AUDIO_TTS_ENGINE=elevenlabs
AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key
AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices
AUDIO_TTS_MODEL=eleven_multilingual_v2
"""
try:
# TODO: Add retries
response = requests.get(
"https://api.elevenlabs.io/v1/voices",
headers={
"xi-api-key": api_key,
"Content-Type": "application/json",
},
)
response.raise_for_status()
voices_data = response.json()
voices = {}
for voice in voices_data.get("voices", []):
voices[voice["voice_id"]] = voice["name"]
except requests.RequestException as e:
# Avoid @lru_cache with exception
log.error(f"Error fetching voices: {str(e)}")
raise RuntimeError(f"Error fetching voices: {str(e)}")
return voices
@router.get("/voices")
async def get_voices(request: Request, user=Depends(get_verified_user)):
return {
"voices": [
{"id": k, "name": v} for k, v in get_available_voices(request).items()
]
}
@@ -3,8 +3,9 @@ import uuid
import time
import datetime
import logging
from aiohttp import ClientSession
from open_webui.apps.webui.models.auths import (
from open_webui.models.auths import (
AddUserForm,
ApiKey,
Auths,
@@ -17,7 +18,7 @@ from open_webui.apps.webui.models.auths import (
UpdateProfileForm,
UserResponse,
)
from open_webui.apps.webui.models.users import Users
from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import (
@@ -29,10 +30,14 @@ from open_webui.env import (
SRC_LOG_LEVELS,
)
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import Response
from fastapi.responses import RedirectResponse, Response
from open_webui.config import (
OPENID_PROVIDER_URL,
ENABLE_OAUTH_SIGNUP,
)
from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.utils import (
from open_webui.utils.auth import (
create_api_key,
create_token,
get_admin_user,
@@ -46,7 +51,7 @@ 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 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars
router = APIRouter()
@@ -165,6 +170,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
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_MAIL = request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL
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
@@ -196,7 +202,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
server = Server(
host=LDAP_SERVER_HOST,
port=LDAP_SERVER_PORT,
get_info=ALL,
get_info=NONE,
use_ssl=LDAP_USE_TLS,
tls=tls,
)
@@ -213,7 +219,11 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
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"],
attributes=[
f"{LDAP_ATTRIBUTE_FOR_USERNAME}",
f"{LDAP_ATTRIBUTE_FOR_MAIL}",
"cn",
],
)
if not search_success:
@@ -221,7 +231,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
entry = connection_app.entries[0]
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
mail = str(entry["mail"])
mail = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
if not mail or mail == "" or mail == "[]":
raise HTTPException(400, f"User {form_data.user} does not have mail.")
cn = str(entry["cn"])
user_dn = entry.entry_dn
@@ -498,8 +510,31 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
@router.get("/signout")
async def signout(response: Response):
async def signout(request: Request, response: Response):
response.delete_cookie("token")
if ENABLE_OAUTH_SIGNUP.value:
oauth_id_token = request.cookies.get("oauth_id_token")
if oauth_id_token:
try:
async with ClientSession() as session:
async with session.get(OPENID_PROVIDER_URL.value) as resp:
if resp.status == 200:
openid_data = await resp.json()
logout_url = openid_data.get("end_session_endpoint")
if logout_url:
response.delete_cookie("oauth_id_token")
return RedirectResponse(
url=f"{logout_url}?id_token_hint={oauth_id_token}"
)
else:
raise HTTPException(
status_code=resp.status,
detail="Failed to fetch OpenID configuration",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {"status": True}
@@ -519,7 +554,6 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
try:
print(form_data)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
@@ -586,8 +620,12 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)):
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"WEBUI_URL": request.app.state.config.WEBUI_URL,
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
"API_KEY_ALLOWED_ENDPOINTS": request.app.state.config.API_KEY_ALLOWED_ENDPOINTS,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
@@ -597,8 +635,12 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
class AdminConfig(BaseModel):
SHOW_ADMIN_DETAILS: bool
WEBUI_URL: str
ENABLE_SIGNUP: bool
ENABLE_API_KEY: bool
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS: bool
API_KEY_ALLOWED_ENDPOINTS: str
ENABLE_CHANNELS: bool
DEFAULT_USER_ROLE: str
JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool
@@ -610,8 +652,18 @@ async def update_admin_config(
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
):
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
request.app.state.config.WEBUI_URL = form_data.WEBUI_URL
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
request.app.state.config.ENABLE_API_KEY = form_data.ENABLE_API_KEY
request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS = (
form_data.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS
)
request.app.state.config.API_KEY_ALLOWED_ENDPOINTS = (
form_data.API_KEY_ALLOWED_ENDPOINTS
)
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
@@ -629,8 +681,12 @@ async def update_admin_config(
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"WEBUI_URL": request.app.state.config.WEBUI_URL,
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
"API_KEY_ALLOWED_ENDPOINTS": request.app.state.config.API_KEY_ALLOWED_ENDPOINTS,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
@@ -642,6 +698,7 @@ class LdapServerConfig(BaseModel):
label: str
host: str
port: Optional[int] = None
attribute_for_mail: str = "mail"
attribute_for_username: str = "uid"
app_dn: str
app_dn_password: str
@@ -658,6 +715,7 @@ async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
"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_mail": request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
"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,
@@ -676,6 +734,7 @@ async def update_ldap_server(
required_fields = [
"label",
"host",
"attribute_for_mail",
"attribute_for_username",
"app_dn",
"app_dn_password",
@@ -694,6 +753,7 @@ async def update_ldap_server(
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_MAIL = form_data.attribute_for_mail
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = (
form_data.attribute_for_username
)
@@ -709,6 +769,7 @@ async def update_ldap_server(
"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_mail": request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
"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,
+710
View File
@@ -0,0 +1,710 @@
import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
from pydantic import BaseModel
from open_webui.socket.main import sio, get_user_ids_from_room
from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels, ChannelModel, ChannelForm
from open_webui.models.messages import (
Messages,
MessageModel,
MessageResponse,
MessageForm,
)
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, get_users_with_access
from open_webui.utils.webhook import post_webhook
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# GetChatList
############################
@router.get("/", response_model=list[ChannelModel])
async def get_channels(user=Depends(get_verified_user)):
if user.role == "admin":
return Channels.get_channels()
else:
return Channels.get_channels_by_user_id(user.id)
############################
# CreateNewChannel
############################
@router.post("/create", response_model=Optional[ChannelModel])
async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user)):
try:
channel = Channels.insert_new_channel(None, form_data, user.id)
return ChannelModel(**channel.model_dump())
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChannelById
############################
@router.get("/{id}", response_model=Optional[ChannelModel])
async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
return ChannelModel(**channel.model_dump())
############################
# UpdateChannelById
############################
@router.post("/{id}/update", response_model=Optional[ChannelModel])
async def update_channel_by_id(
id: str, form_data: ChannelForm, user=Depends(get_admin_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
try:
channel = Channels.update_channel_by_id(id, form_data)
return ChannelModel(**channel.model_dump())
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# DeleteChannelById
############################
@router.delete("/{id}/delete", response_model=bool)
async def delete_channel_by_id(id: str, user=Depends(get_admin_user)):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
try:
Channels.delete_channel_by_id(id)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChannelMessages
############################
class MessageUserResponse(MessageResponse):
user: UserNameResponse
@router.get("/{id}/messages", response_model=list[MessageUserResponse])
async def get_channel_messages(
id: str, skip: int = 0, limit: int = 50, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message_list = Messages.get_messages_by_channel_id(id, skip, limit)
users = {}
messages = []
for message in message_list:
if message.user_id not in users:
user = Users.get_user_by_id(message.user_id)
users[message.user_id] = user
replies = Messages.get_replies_by_message_id(message.id)
latest_reply_at = replies[0].created_at if replies else None
messages.append(
MessageUserResponse(
**{
**message.model_dump(),
"reply_count": len(replies),
"latest_reply_at": latest_reply_at,
"reactions": Messages.get_reactions_by_message_id(message.id),
"user": UserNameResponse(**users[message.user_id].model_dump()),
}
)
)
return messages
############################
# PostNewMessage
############################
async def send_notification(webui_url, channel, message, active_user_ids):
users = get_users_with_access("read", channel.access_control)
for user in users:
if user.id in active_user_ids:
continue
else:
if user.settings:
webhook_url = user.settings.ui.get("notifications", {}).get(
"webhook_url", None
)
if webhook_url:
post_webhook(
webhook_url,
f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}",
{
"action": "channel",
"message": message.content,
"title": channel.name,
"url": f"{webui_url}/channels/{channel.id}",
},
)
@router.post("/{id}/messages/post", response_model=Optional[MessageModel])
async def post_new_message(
request: Request,
id: str,
form_data: MessageForm,
background_tasks: BackgroundTasks,
user=Depends(get_verified_user),
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
try:
message = Messages.insert_new_message(form_data, channel.id, user.id)
if message:
event_data = {
"channel_id": channel.id,
"message_id": message.id,
"data": {
"type": "message",
"data": MessageUserResponse(
**{
**message.model_dump(),
"reply_count": 0,
"latest_reply_at": None,
"reactions": Messages.get_reactions_by_message_id(
message.id
),
"user": UserNameResponse(**user.model_dump()),
}
).model_dump(),
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
}
await sio.emit(
"channel-events",
event_data,
to=f"channel:{channel.id}",
)
if message.parent_id:
# If this message is a reply, emit to the parent message as well
parent_message = Messages.get_message_by_id(message.parent_id)
if parent_message:
await sio.emit(
"channel-events",
{
"channel_id": channel.id,
"message_id": parent_message.id,
"data": {
"type": "message:reply",
"data": MessageUserResponse(
**{
**parent_message.model_dump(),
"user": UserNameResponse(
**Users.get_user_by_id(
parent_message.user_id
).model_dump()
),
}
).model_dump(),
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
},
to=f"channel:{channel.id}",
)
active_user_ids = get_user_ids_from_room(f"channel:{channel.id}")
background_tasks.add_task(
send_notification,
request.app.state.config.WEBUI_URL,
channel,
message,
active_user_ids,
)
return MessageModel(**message.model_dump())
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChannelMessage
############################
@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageUserResponse])
async def get_channel_message(
id: str, message_id: str, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message = Messages.get_message_by_id(message_id)
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if message.channel_id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
return MessageUserResponse(
**{
**message.model_dump(),
"user": UserNameResponse(
**Users.get_user_by_id(message.user_id).model_dump()
),
}
)
############################
# GetChannelThreadMessages
############################
@router.get(
"/{id}/messages/{message_id}/thread", response_model=list[MessageUserResponse]
)
async def get_channel_thread_messages(
id: str,
message_id: str,
skip: int = 0,
limit: int = 50,
user=Depends(get_verified_user),
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message_list = Messages.get_messages_by_parent_id(id, message_id, skip, limit)
users = {}
messages = []
for message in message_list:
if message.user_id not in users:
user = Users.get_user_by_id(message.user_id)
users[message.user_id] = user
messages.append(
MessageUserResponse(
**{
**message.model_dump(),
"reply_count": 0,
"latest_reply_at": None,
"reactions": Messages.get_reactions_by_message_id(message.id),
"user": UserNameResponse(**users[message.user_id].model_dump()),
}
)
)
return messages
############################
# UpdateMessageById
############################
@router.post(
"/{id}/messages/{message_id}/update", response_model=Optional[MessageModel]
)
async def update_message_by_id(
id: str, message_id: str, form_data: MessageForm, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message = Messages.get_message_by_id(message_id)
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if message.channel_id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
try:
message = Messages.update_message_by_id(message_id, form_data)
message = Messages.get_message_by_id(message_id)
if message:
await sio.emit(
"channel-events",
{
"channel_id": channel.id,
"message_id": message.id,
"data": {
"type": "message:update",
"data": MessageUserResponse(
**{
**message.model_dump(),
"user": UserNameResponse(
**user.model_dump()
).model_dump(),
}
).model_dump(),
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
},
to=f"channel:{channel.id}",
)
return MessageModel(**message.model_dump())
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# AddReactionToMessage
############################
class ReactionForm(BaseModel):
name: str
@router.post("/{id}/messages/{message_id}/reactions/add", response_model=bool)
async def add_reaction_to_message(
id: str, message_id: str, form_data: ReactionForm, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message = Messages.get_message_by_id(message_id)
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if message.channel_id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
try:
Messages.add_reaction_to_message(message_id, user.id, form_data.name)
message = Messages.get_message_by_id(message_id)
await sio.emit(
"channel-events",
{
"channel_id": channel.id,
"message_id": message.id,
"data": {
"type": "message:reaction:add",
"data": {
**message.model_dump(),
"user": UserNameResponse(
**Users.get_user_by_id(message.user_id).model_dump()
).model_dump(),
"name": form_data.name,
},
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
},
to=f"channel:{channel.id}",
)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# RemoveReactionById
############################
@router.post("/{id}/messages/{message_id}/reactions/remove", response_model=bool)
async def remove_reaction_by_id_and_user_id_and_name(
id: str, message_id: str, form_data: ReactionForm, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message = Messages.get_message_by_id(message_id)
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if message.channel_id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
try:
Messages.remove_reaction_by_id_and_user_id_and_name(
message_id, user.id, form_data.name
)
message = Messages.get_message_by_id(message_id)
await sio.emit(
"channel-events",
{
"channel_id": channel.id,
"message_id": message.id,
"data": {
"type": "message:reaction:remove",
"data": {
**message.model_dump(),
"user": UserNameResponse(
**Users.get_user_by_id(message.user_id).model_dump()
).model_dump(),
"name": form_data.name,
},
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
},
to=f"channel:{channel.id}",
)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# DeleteMessageById
############################
@router.delete("/{id}/messages/{message_id}/delete", response_model=bool)
async def delete_message_by_id(
id: str, message_id: str, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message = Messages.get_message_by_id(message_id)
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if message.channel_id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
try:
Messages.delete_message_by_id(message_id)
await sio.emit(
"channel-events",
{
"channel_id": channel.id,
"message_id": message.id,
"data": {
"type": "message:delete",
"data": {
**message.model_dump(),
"user": UserNameResponse(**user.model_dump()).model_dump(),
},
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
},
to=f"channel:{channel.id}",
)
if message.parent_id:
# If this message is a reply, emit to the parent message as well
parent_message = Messages.get_message_by_id(message.parent_id)
if parent_message:
await sio.emit(
"channel-events",
{
"channel_id": channel.id,
"message_id": parent_message.id,
"data": {
"type": "message:reply",
"data": MessageUserResponse(
**{
**parent_message.model_dump(),
"user": UserNameResponse(
**Users.get_user_by_id(
parent_message.user_id
).model_dump()
),
}
).model_dump(),
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
},
to=f"channel:{channel.id}",
)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
@@ -2,15 +2,15 @@ import json
import logging
from typing import Optional
from open_webui.apps.webui.models.chats import (
from open_webui.models.chats import (
ChatForm,
ChatImportForm,
ChatResponse,
Chats,
ChatTitleIdResponse,
)
from open_webui.apps.webui.models.tags import TagModel, Tags
from open_webui.apps.webui.models.folders import Folders
from open_webui.models.tags import TagModel, Tags
from open_webui.models.folders import Folders
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES
@@ -19,7 +19,7 @@ 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.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_permission
log = logging.getLogger(__name__)
@@ -463,6 +463,30 @@ async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
)
############################
# CloneSharedChatById
############################
@router.post("/{id}/clone/shared", response_model=Optional[ChatResponse])
async def clone_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_share_id(id)
if chat:
updated_chat = {
**chat.chat,
"originalChatId": chat.id,
"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())
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# ArchiveChat
############################
@@ -607,7 +631,6 @@ async def add_tag_by_id_and_tag_name(
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
@@ -3,7 +3,7 @@ from pydantic import BaseModel
from typing import Optional
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.config import get_config, save_config
from open_webui.config import BannerModel
@@ -2,8 +2,8 @@ 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 (
from open_webui.models.users import Users, UserModel
from open_webui.models.feedbacks import (
FeedbackModel,
FeedbackResponse,
FeedbackForm,
@@ -11,7 +11,7 @@ from open_webui.apps.webui.models.feedbacks import (
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.utils.auth import get_admin_user, get_verified_user
router = APIRouter()
@@ -5,27 +5,28 @@ from pathlib import Path
from typing import Optional
from pydantic import BaseModel
import mimetypes
from urllib.parse import quote
from open_webui.storage.provider import Storage
from open_webui.apps.webui.models.files import (
from open_webui.models.files import (
FileForm,
FileModel,
FileModelResponse,
Files,
)
from open_webui.apps.retrieval.main import process_file, ProcessFileForm
from open_webui.routers.retrieval import process_file, ProcessFileForm
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 import APIRouter, Depends, File, HTTPException, UploadFile, status, Request
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.utils.auth import get_admin_user, get_verified_user
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -39,7 +40,9 @@ router = APIRouter()
@router.post("/", response_model=FileModelResponse)
def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
def upload_file(
request: Request, file: UploadFile = File(...), user=Depends(get_verified_user)
):
log.info(f"file.content_type: {file.content_type}")
try:
unsanitized_filename = file.filename
@@ -68,7 +71,7 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
)
try:
process_file(ProcessFileForm(file_id=id))
process_file(request, ProcessFileForm(file_id=id))
file_item = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
@@ -183,13 +186,15 @@ class ContentForm(BaseModel):
@router.post("/{id}/data/content/update")
async def update_file_data_content_by_id(
id: str, form_data: ContentForm, user=Depends(get_verified_user)
request: Request, 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))
process_file(
request, ProcessFileForm(file_id=id, content=form_data.content)
)
file = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
@@ -218,11 +223,22 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
# 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)}"'
}
# Handle Unicode filenames
filename = file.meta.get("name", file.filename)
encoded_filename = quote(filename) # RFC5987 encoding
headers = {}
if file.meta.get("content_type") not in [
"application/pdf",
"text/plain",
]:
headers = {
**headers,
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
}
return FileResponse(file_path, headers=headers)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -279,16 +295,20 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
if file and (file.user_id == user.id or user.role == "admin"):
file_path = file.path
# Handle Unicode filenames
filename = file.meta.get("name", file.filename)
encoded_filename = quote(filename) # RFC5987 encoding
headers = {
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
}
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}")
headers = {
"Content-Disposition": f'attachment; filename="{file.meta.get("name", file.filename)}"'
}
return FileResponse(file_path, headers=headers)
else:
raise HTTPException(
@@ -307,7 +327,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
return StreamingResponse(
generator(),
media_type="text/plain",
headers={"Content-Disposition": f"attachment; filename={file_name}"},
headers=headers,
)
else:
raise HTTPException(
@@ -325,10 +345,12 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file and (file.user_id == user.id or user.role == "admin"):
# We should add Chroma cleanup here
result = Files.delete_file_by_id(id)
if result:
try:
Storage.delete_file(file.filename)
Storage.delete_file(file.path)
except Exception as e:
log.exception(e)
log.error(f"Error deleting files")

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