Compare commits

..
306 Commits
Author SHA1 Message Date
Timothy Jaeryang Baek 95cadaca72 Merge pull request #10933 from open-webui/dev
0.5.18
2025-02-27 12:00:52 -08:00
Timothy Jaeryang Baek e26c443815 0.5.18 2025-02-27 12:00:23 -08:00
Timothy Jaeryang Baek 046f1fb30a Merge pull request #10927 from open-webui/dev
0.5.18
2025-02-27 11:50:08 -08:00
Timothy Jaeryang Baek 63d825692e doc: changelog 2025-02-27 11:49:59 -08:00
Timothy Jaeryang Baek 70667d5cc8 refac 2025-02-27 11:45:25 -08:00
Timothy Jaeryang Baek 2ac0eae99e Merge pull request #10925 from SleepyYui/fix-10905
fix: Fix OpenAI connection not actually deleting
2025-02-27 11:44:20 -08:00
Timothy Jaeryang Baek 1dd442f38b Merge pull request #10919 from df-cgdm/main
**fix** Resolve a bug in /api/models/base which was not returning openai models when ENABLE_FORWARD_USER_INFO_HEADERS is true
2025-02-27 11:42:51 -08:00
Yui 94c19f73b0 add config update when deleting openai model 2025-02-27 20:41:30 +01:00
Timothy Jaeryang Baek ed0b9be4e3 fix 2025-02-27 11:39:59 -08:00
Timothy Jaeryang Baek 866d02bbae refac 2025-02-27 11:39:00 -08:00
Timothy Jaeryang Baek f7c5cd486b Merge pull request #10908 from barnabehvrd/main
i18n: Updated translations for fr-FR
2025-02-27 11:37:58 -08:00
Timothy Jaeryang Baek 660a44c918 fix: insecure connection over LAN 2025-02-27 11:37:44 -08:00
Didier FOURNOUT b6873153c1 resolve a bug in /api/models/base not returning openai models when ENABLE_FORWARD_USER_INFO_HEADERS is true 2025-02-27 19:32:15 +00:00
df-cgdm 24a2e84692 Merge branch 'open-webui:main' into main 2025-02-27 19:13:00 +00:00
v0ltis f643182eb0 Updated french translation 2025-02-27 19:04:53 +01:00
Timothy Jaeryang Baek 38fb9d5fd8 chore: jspdf depedency 2025-02-27 03:54:16 -08:00
Timothy Jaeryang Baek 15485e7c5d Merge pull request #10469 from open-webui/dev
0.5.17
2025-02-27 03:46:08 -08:00
Timothy Jaeryang Baek 8241fa2e12 doc: changelog 2025-02-27 03:45:33 -08:00
Timothy Jaeryang Baek 37a7dd6d33 chore: format 2025-02-27 02:18:44 -08:00
Timothy Jaeryang Baek 85b7ec9387 Merge pull request #10873 from TiancongLx/dev
i18n: update zh-TW
2025-02-27 02:17:33 -08:00
Tiancong Li b838a1df99 i18n: update zh-TW 2025-02-27 17:57:10 +08:00
Timothy Jaeryang Baek 8c4d967ef4 refac: evaluations settings ui 2025-02-26 23:58:43 -08:00
Timothy Jaeryang Baek ce7cf62a55 refac: dedup 2025-02-26 23:51:39 -08:00
Timothy Jaeryang Baek 0a4dbf7cf0 refac 2025-02-26 23:41:09 -08:00
Timothy Jaeryang Baek fcbdfbd744 refac 2025-02-26 23:35:09 -08:00
Timothy Jaeryang Baek 1ae702b8a6 Merge pull request #10860 from open-webui/audit-log-dev
feat: add audit logging feature
2025-02-26 23:19:37 -08:00
Timothy Jaeryang Baek 2d379cb35f chore: pyproject 2025-02-26 23:19:19 -08:00
Timothy Jaeryang Baek 76e90d9f3f Merge pull request #10436 from victorstevansuse/feat/audits
feat: add audit logging feature
2025-02-26 23:15:10 -08:00
Timothy Jaeryang Baekandsilentoplayz da8c42ee49 feat: Sort models by name in ascending/descending order
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-02-26 23:12:46 -08:00
Timothy Jaeryang Baek ddb30589e3 chore: format
HIDE MODELS
2025-02-26 22:18:18 -08:00
Timothy Jaeryang Baek ef734833a5 fix 2025-02-26 21:22:51 -08:00
Timothy Jaeryang Baek 40ddd172e0 refac: web search settings ui 2025-02-26 15:56:45 -08:00
Timothy Jaeryang Baek 57010901e6 enh: bypass embedding and retrieval 2025-02-26 15:42:19 -08:00
Timothy Jaeryang Baek 1c2e36f1b7 refac 2025-02-26 13:59:08 -08:00
Timothy Jaeryang Baek 34aeaaf020 refac 2025-02-26 13:54:26 -08:00
Timothy Jaeryang Baek fa91d83ac3 refac: documents settings ui 2025-02-26 13:48:56 -08:00
Timothy Jaeryang Baek 78a8ef8e66 refac: audio file handling 2025-02-26 13:09:52 -08:00
Timothy Jaeryang Baek 46ac6f2b29 fix 2025-02-26 12:53:07 -08:00
Timothy Jaeryang Baek 33d3558ca9 Merge pull request #10817 from NovoNordisk-OpenSource/ivaroli/adding-json-as-supported-file-type
fix: Using the TextLoader instead of Tika for JSON files
2025-02-26 12:49:29 -08:00
Timothy Jaeryang Baek c1c92b0c03 Merge pull request #10820 from oyve/dev
i18n: Update translation.json for nb-NO
2025-02-26 12:49:02 -08:00
Øyvind 618307f35b Update translation.json
Fill all empty translation
2025-02-26 16:29:47 +01:00
Ívar Óli Sigurðsson c5a09cdd21 adding a comma 2025-02-26 15:27:03 +01:00
Ívar Óli Sigurðsson 661711164a Adding json as a known source for Tika 2025-02-26 15:11:21 +01:00
Timothy Jaeryang Baek 80efaaffdd Merge pull request #10810 from panda44312/patch-1
i18n: Update zh-cn translation
2025-02-26 04:29:41 -08:00
Panda 46f2dc0d11 Update zh-cn 2025-02-26 12:24:52 +01:00
Timothy Jaeryang Baek 674d6e08fc Merge pull request #10809 from TobiasGoerke/feat/update_timestamp_asynchronously
feat: update get_current_user to refresh last active timestamp asynchronously
2025-02-26 02:38:06 -08:00
Tobias Goerke 76891f4760 feat: update get_current_user to refresh last active timestamp asynchronously 2025-02-26 10:53:47 +01:00
Timothy Jaeryang Baek 20f0b02b38 Merge pull request #10755 from softue/check-if-enable-ldap-chroma
Conditional import of ldap3 and chromadb
2025-02-25 16:02:40 -08:00
Timothy Jaeryang Baek bef7a532cc refac 2025-02-25 16:01:29 -08:00
Timothy Jaeryang Baek ced508c912 Merge pull request #10783 from silentoplayz/share-fix
fix: community sharing for prompts and tools
2025-02-25 15:35:11 -08:00
Timothy Jaeryang Baek c883368842 Merge pull request #10786 from silentoplayz/confirm-clear-for-memories
feat: Confirmation prompt on 'Clear' for memories
2025-02-25 15:34:14 -08:00
Silentoplayz 8097b6a3ff Update ManageModal.svelte
no confirmation when no memories exist
2025-02-25 18:00:59 -05:00
Silentoplayz 7cfbf6489a Update ManageModal.svelte
feat: Add ConfirmDialog to 'Clear Memory' button
2025-02-25 17:53:13 -05:00
Silentoplayz 11967a780d Share fix
Fix
2025-02-25 17:05:05 -05:00
Timothy Jaeryang Baek 6cf0ceedba refac 2025-02-25 11:11:36 -08:00
Timothy Jaeryang Baek ec72dcba75 Merge pull request #10742 from hjer/improve-swedish-translation
i18n: Add and improve Swedish translation
2025-02-25 10:54:19 -08:00
Timothy Jaeryang Baek 54d7bf8f28 Merge branch 'dev' into improve-swedish-translation 2025-02-25 10:54:08 -08:00
Timothy Jaeryang Baek 3be5e3129b Merge pull request #10752 from NovoNordisk-OpenSource/yvedeng/standardize-logging
refactor: replace print statements with logging
2025-02-25 10:53:02 -08:00
Rômulo Mendes Figueiredo 2298bfbd7e refac: conditional import of ldap3 2025-02-25 13:07:53 -03:00
Yifang Deng 0e5d5ecb81 refactor: replace print statements with logging for better error tracking 2025-02-25 15:53:55 +01:00
Rômulo Mendes Figueiredo b52a7e3543 refac: conditional import of chromadb 2025-02-25 11:35:36 -03:00
hjer df1238229c add and improve swedish translation 2025-02-25 13:58:09 +01:00
Timothy Jaeryang Baek eafefcf838 chore: bump 2025-02-25 01:46:57 -08:00
Timothy Jaeryang Baek 9f27d7710b chore: format 2025-02-25 01:46:08 -08:00
Timothy Jaeryang Baek 7c68a424f8 refac 2025-02-25 01:44:39 -08:00
Timothy Jaeryang Baek 5d3249848d refac: allow image only message via enter 2025-02-25 01:40:17 -08:00
Timothy Jaeryang Baek 7e2b2ec761 fix: openai usage display issue 2025-02-25 01:35:59 -08:00
Timothy Jaeryang Baek b964b175d4 refac 2025-02-25 01:03:15 -08:00
Timothy Jaeryang Baek 46c4da4864 enh: "stream" hook 2025-02-25 01:00:29 -08:00
df-cgdm 126045318f Merge branch 'open-webui:main' into main 2025-02-25 07:37:57 +00:00
Timothy Jaeryang Baek 205013da2f Merge pull request #10683 from grandtele/fix/openai-o1-legacy-roles
fix: restore compatibility for older o1 models (o1-mini, o1-preview)
2025-02-24 23:35:18 -08:00
Timothy Jaeryang Baek abf36c0645 Merge pull request #10711 from jasonpnnl/send-usage-to-pipeline-outlet
fix: Include usage in chatCompleted handler for pipeline outlet functions
2025-02-24 23:34:18 -08:00
Timothy Jaeryang Baek e06111a362 refac 2025-02-24 23:21:03 -08:00
Timothy Jaeryang Baek cd3904046d refac 2025-02-24 20:19:32 -08:00
Jason E. Jensen 89aaf64209 add optional usage to chatCompleted messages 2025-02-24 22:32:08 +00:00
Timothy Jaeryang Baek 95ae2aeef3 Merge pull request #10704 from TiancongLx/dev
i18n: update zh-TW
2025-02-24 12:11:27 -08:00
Tiancong Li cefe274f74 i18n: update zh-TW 2025-02-25 04:03:16 +08:00
Timothy Jaeryang Baek d8bc3098db Merge pull request #9918 from df-cgdm/main
feat: Add  X-OpenWebUI when forwarding to ollama servers
2025-02-24 11:55:04 -08:00
Timothy Jaeryang Baek 628b89009c Merge pull request #10690 from hurxxxx/feat/onedrive-file-attach
feat: OneDrive file picker integration
2025-02-24 11:52:37 -08:00
Timothy Jaeryang Baek 23ff2988bf Merge branch 'dev' into feat/onedrive-file-attach 2025-02-24 11:51:59 -08:00
Timothy Jaeryang Baek e0c199255f Merge pull request #10666 from EkaterinePapava/main
i18n: Update Georgian translation
2025-02-24 11:37:18 -08:00
hurxxxx b2422b3c8f i18n: onedrive related 2025-02-25 01:56:33 +09:00
hurxxxx 4cc3102758 feat: onedrive file picker integration 2025-02-25 01:47:07 +09:00
grand 8c020488dd * fix: restore compatibility for older o1 models (o1-mini, o1-preview) 2025-02-24 12:04:06 +01:00
Gunwoo Hur 0335d479f9 feat: add onedrive file picker 2025-02-24 17:27:37 +09:00
Ekaterine Papava dcaab9d6b5 Update Georgian translation 2025-02-24 09:07:45 +01:00
Timothy Jaeryang Baek 0ba45ad82b Merge pull request #10617 from grandtele/fix/openai-o1-o3-role
fix: replace "system" with "developer" for o1 and o3 models
2025-02-23 22:33:59 -08:00
Timothy Jaeryang Baek 802f8809dd fix: codeblock 2025-02-23 21:39:34 -08:00
Timothy Jaeryang Baek 7a63947b94 fix: tailwind v4 ios 16.4 compatibility issue 2025-02-23 18:41:33 -08:00
Timothy Jaeryang Baek 737b1723c7 refac 2025-02-23 14:12:41 -08:00
Timothy Jaeryang Baek e90e254575 Merge pull request #10605 from silentoplayz/shortcuts-modal-update
feat: Add more keyboard shortcuts
2025-02-23 13:48:55 -08:00
Timothy Jaeryang Baek f8ad74fc42 refac 2025-02-23 13:32:19 -08:00
Timothy Jaeryang Baek 6c862b8fbf Merge pull request #10619 from OrenZhang/feat_s3_virtual_path
feat(s3): support for s3 virtual path style
2025-02-23 13:27:22 -08:00
Timothy Jaeryang Baek 4b908ea626 Merge pull request #10607 from TiancongLx/dev
i18n: update zh-TW
2025-02-23 13:24:04 -08:00
Timothy Jaeryang Baek b77937dfad Merge pull request #10634 from sebdanielsson/handle-no-location-access
fix: Handle no location permission
2025-02-23 13:23:47 -08:00
Sebastian b00e14eb3f Handle no location permission 2025-02-23 20:27:55 +01:00
orenzhang 9748a030f7 Merge remote-tracking branch 'oui/dev' into feat_s3_virtual_path 2025-02-23 20:54:04 +08:00
orenzhang 1060d62add refactor(s3): code lint 2025-02-23 20:52:22 +08:00
orenzhang 4c3dd84b32 feat(s3): support for s3 virtual path style 2025-02-23 20:31:08 +08:00
grand 82657acdce * fix: adjust role mapping for o1 and o3 models in openai_o1_o3_handler 2025-02-23 13:20:45 +01:00
Tiancong Li c96cc831d5 i18n: update zh-TW 2025-02-23 14:29:51 +08:00
Silentoplayz e41a5278f7 Update ShortcutsModal.svelte
fix
2025-02-22 23:00:06 -05:00
Silentoplayz b5aa562039 Update ShortcutsModal.svelte
Added more shortcuts
2025-02-22 22:50:37 -05:00
Silentoplayz 5281aeb07b Merge branch 'dev' of https://github.com/silentoplayz/open-webui into dev 2025-02-22 21:17:56 -05:00
Timothy Jaeryang Baek 39717812ee Merge pull request #10572 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-02-22 14:31:24 -08:00
Timothy Jaeryang Baek 9f6e182400 Merge pull request #10573 from dom133/dev
i18n: Update Polish Translation
2025-02-22 14:30:27 -08:00
Timothy Jaeryang Baek e4ba8fea8a Merge pull request #10583 from hurxxxx/feat/show_at_model_suggestions_to_placeholder
feat: Show atSelectedModel suggestions in Placeholder
2025-02-22 14:29:52 -08:00
hurxxxx f80aea60c7 fix: Fix incorrect object property usage 2025-02-23 00:19:51 +09:00
hurxxxx a407558375 feat: Show suggestions when selecting the atModel. 2025-02-22 23:27:13 +09:00
dom133 65aa41ab81 i18n: Update Polish Translation 2025-02-22 13:10:30 +01:00
Kylapaallikko 6331963e66 Update translation.json 2025-02-22 13:34:35 +02:00
Timothy Jaeryang Baek 32fa8d5143 Merge pull request #10566 from oloflarsson/feat/async-pipes
feat: add optional support for async pipes
2025-02-22 02:47:08 -08:00
Timothy Jaeryang Baek 8f3fcdcf5c refac 2025-02-22 02:00:06 -08:00
Timothy Jaeryang Baek 794919e91d refac 2025-02-22 01:22:17 -08:00
Timothy Jaeryang Baek 667d26ca12 refac 2025-02-22 01:16:58 -08:00
Timothy Jaeryang Baek 50dec12072 refac 2025-02-21 22:15:22 -08:00
Timothy Jaeryang Baek 8fb6b03ddc Merge pull request #10555 from pwnless/dev
fix: ollama backend native tool calling
2025-02-21 21:09:51 -08:00
hopeless 7bfa29fa81 Update payload.py
Fixes ollama native tool calling because native tool calling content will be str '', and tool call processing will be completely ignored.
2025-02-22 12:13:14 +08:00
Timothy Jaeryang Baek 9bada6421e refac: code block image styling 2025-02-21 16:39:56 -08:00
Timothy Jaeryang Baekandharry zhou b14e75dd6c feat: added Trust Proxy Environment switch in Web Search admin settings tab.
Co-Authored-By: harry zhou <67385896+harryzhou2000@users.noreply.github.com>
2025-02-21 13:40:11 -08:00
Timothy Jaeryang Baek 6623583004 Merge pull request #10549 from jeannotdamoiseaux/fix/oauth-groups-default-typeerror
Fix: Ensure `user_oauth_groups` defaults to an empty list to prevent TypeError
2025-02-21 13:37:35 -08:00
Jeannot Damoiseaux d50098b622 Fix: Ensure user_oauth_groups defaults to an empty list to prevent TypeError
When the OAuth groups claim does not yield a list, `user_oauth_groups` was previously
set to None, causing a TypeError during membership checks. Changed this default to
an empty list (`[]`) to ensure the variable is always iterable, preventing errors
for non-admin users while logging in.

This fix ensures stability in the `update_user_groups` function.
2025-02-21 22:25:22 +01:00
Timothy Jaeryang Baek 642dcd4b70 fix: model import 2025-02-21 13:19:11 -08:00
Timothy Jaeryang Baek a7d8ed0c6d refac 2025-02-21 12:11:21 -08:00
Timothy Jaeryang Baek ab1b910d80 Merge pull request #10486 from Micca/feature/document_intelligence_support
Feat: Adding Support for Azure AI Document Intelligence for Content Extraction (Revised)
2025-02-21 10:56:18 -08:00
Timothy Jaeryang Baek a3c2ff61ed Merge pull request #10483 from KarlLee830/translate
i18n: Update Chinese Translation
2025-02-21 10:55:55 -08:00
Timothy Jaeryang Baek b111434fa9 Merge pull request #10488 from glimpse-0201/dev
fix:Quick selection tool lost
2025-02-21 10:55:44 -08:00
Timothy Jaeryang Baek 613a087387 refac 2025-02-21 10:55:03 -08:00
Timothy Jaeryang Baek b0a19a9801 Merge pull request #10501 from Synergyst/feature/openai-tts-custom-url-fix-dev
fix: Custom OpenAI-TTS URL to fetch actual voices and models
2025-02-21 10:52:41 -08:00
Timothy Jaeryang Baek 086a78f863 Merge pull request #10521 from Seniorsimo/add-stop-parameter
**fix** Added support for stop parameter in OpenAI to Ollama conversion
2025-02-21 10:51:49 -08:00
Timothy Jaeryang Baek b4c64a21b3 Merge pull request #10513 from mcelrath/dev
Support thinking/solution tags used by Openthinker
2025-02-21 10:49:45 -08:00
Simone fb3886cf04 Added support for stop parameter 2025-02-21 18:46:10 +01:00
Bob McElrath c9079d4b4d Merge branch 'open-webui:dev' into dev 2025-02-21 09:13:53 -05:00
Bob McElrath c4b441de65 Support thinking tags used by Openthinker 2025-02-21 09:12:34 -05:00
Synergyst f789ad59a9 Update audio.py
Removed original code that was commented out
2025-02-21 04:47:46 -06:00
Coleton M cdf620e6ee Update audio.py to fetch custom URL voices and models 2025-02-21 04:41:45 -06:00
huanght 2993332b38 fix:Quick selection tool lost 2025-02-21 16:10:11 +08:00
Olof Larsson ab1f2ae914 feat/async-pipes 2025-02-21 08:24:48 +01:00
Karl Lee 68e33b5386 i18n: Update Chinese Translation 2025-02-21 14:58:40 +08:00
Timothy Jaeryang Baek e31f680788 refac 2025-02-20 20:46:00 -08:00
Timothy Jaeryang Baek 1bf16ec23d Merge pull request #10462 from Seniorsimo/fix-ollama-stream-conversion
**fix** ollama to openai conversion on one shot streamed response
2025-02-20 13:30:29 -08:00
Timothy Jaeryang Baek 2697a4d7f8 Merge pull request #10460 from aleixdorca/dev
Update catalan translation.json
2025-02-20 13:30:00 -08:00
Timothy Jaeryang Baek 8629f6b0f3 Merge pull request #10466 from HuangruiChu/main
i18n: Update zh-Chinese translation
2025-02-20 13:29:49 -08:00
Richard (Huangrui) Chu 1332a0d381 Update zh-CN translation
1. remove the typo ”‘’"  for -1 表示无限制,正整数表示具体限制”
2. Token should be kept rather than translated as "标记"
3. Max Tokens (num_predict) should be "最大Token数量 (num_predict)"
4. "Enter Jupyter Token" here “Token" could be translated as ”令牌“ just as 	"JWT Token": "JWT 令牌", (line 582)
5. "TLS" which means "Transport Layer Security" should be translated to "传输层安全协议"
6. "Tokens To Keep On Context Refresh (num_keep)" "在语境刷新时需保留的 Token 数量"
7. change token to "Token" in the Chinese translation.
2025-02-20 16:07:32 -05:00
Simone a8859a8145 Fix on ollama to openai conversion - stream can return a single message with content 2025-02-20 21:25:32 +01:00
Timothy Jaeryang Baek 6fedd72e39 Merge pull request #10459 from open-webui/dev
0.5.16
2025-02-20 11:26:36 -08:00
Timothy Jaeryang Baek 472a291c24 doc: changelog 2025-02-20 11:25:17 -08:00
Aleix Dorca 08df5f6d7c Update catalan translation.json 2025-02-20 20:24:58 +01:00
Timothy Jaeryang Baek b124c15018 Merge pull request #10431 from EntropyYue/dev
i18n: Update Chinese translation
2025-02-20 11:19:18 -08:00
Timothy Jaeryang Baek 0a7a265ff5 Merge pull request #10445 from PkmX/pr-fix-web-search-proxy
fix: correctly interpret RAG_WEB_SEARCH_TRUST_ENV as bool
2025-02-20 11:16:47 -08:00
Timothy Jaeryang Baek 93d486d50e revert: faulty dedup code 2025-02-20 11:02:45 -08:00
PkmX af10aba805 fix: correctly interpret RAG_WEB_SEARCH_TRUST_ENV as bool 2025-02-21 01:46:17 +08:00
Victor Ribeiro 39c6ecb50d feat: add audit logger middleware and custom logger using loguru 2025-02-20 11:15:38 -03:00
EntropyYue 89b4627e9a Update translation.json 2025-02-20 21:47:21 +08:00
EntropyYue e920f96b17 Update translation.json 2025-02-20 21:12:04 +08:00
Timothy Jaeryang Baek cf4bbcde14 Merge pull request #10421 from open-webui/dev
fix: workflow
2025-02-20 02:10:02 -08:00
Timothy Jaeryang Baek c882aacc23 fix: workflow 2025-02-20 02:09:44 -08:00
Timothy Jaeryang Baek 624f2ebcff Merge pull request #10420 from open-webui/dependabot/pip/pip-e8a89e233e
build(deps): bump the pip group across 2 directories with 1 update
2025-02-20 02:03:07 -08:00
dependabot[bot] eb218f6674 build(deps): bump the pip group across 2 directories with 1 update
Bumps the pip group with 1 update in the / directory: [python-jose](https://github.com/mpdavis/python-jose).
Bumps the pip group with 1 update in the /backend directory: [python-jose](https://github.com/mpdavis/python-jose).


Updates `python-jose` from 3.3.0 to 3.4.0
- [Release notes](https://github.com/mpdavis/python-jose/releases)
- [Changelog](https://github.com/mpdavis/python-jose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mpdavis/python-jose/compare/3.3.0...3.4.0)

Updates `python-jose` from 3.3.0 to 3.4.0
- [Release notes](https://github.com/mpdavis/python-jose/releases)
- [Changelog](https://github.com/mpdavis/python-jose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mpdavis/python-jose/compare/3.3.0...3.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 10:02:35 +00:00
Timothy Jaeryang Baek 9fdf2cd16c Merge pull request #10330 from open-webui/dev
0.5.15
2025-02-20 02:01:29 -08:00
Timothy Jaeryang Baek 385a894b3c chore: pyproject 2025-02-20 01:42:11 -08:00
Timothy Jaeryang Baek 2d5b685429 doc: changelog 2025-02-20 01:39:50 -08:00
Timothy Jaeryang Baek 900dc1f0a3 chore: bump 2025-02-20 01:28:46 -08:00
Timothy Jaeryang Baek eeb00a5ca2 chore: format 2025-02-20 01:01:29 -08:00
Timothy Jaeryang Baek 2b913a99a3 Merge pull request #10417 from hangxingliu/liu/fix-build-proxy
build: load the proxy from the system env for Pyodide to download packages
2025-02-20 00:58:27 -08:00
Timothy Jaeryang Baek 0994a07874 Merge pull request #10415 from labanzu/better-pdf-preview
feat: better pdf preview
2025-02-20 00:57:11 -08:00
Timothy Jaeryang Baek a818d8e3b7 Merge pull request #10413 from dom133/dev
i18n: Improve Polish translation
2025-02-20 00:55:52 -08:00
Liu Yue 3ae7302f7b build: load the proxy from the system env for Pyodide to download packages #6234 2025-02-20 16:40:31 +08:00
dom133 9ba03523e7 i18n: Improve Polish translation 2025-02-20 09:26:07 +01:00
labanzu f4e67bf2d3 update 2025-02-20 16:10:29 +08:00
labanzu a3d3ca293e chore: optimize pdf view 2025-02-20 15:55:18 +08:00
Timothy Jaeryang Baek d6f3444141 fix: katex issue 2025-02-19 23:37:11 -08:00
Timothy Jaeryang Baek 0e20b15afe Merge pull request #10410 from xinhai-ai/dev
fix: GitHub OAuth email retrieval when public email is not set
2025-02-19 23:16:46 -08:00
Timothy Jaeryang Baek e1cc104805 Merge pull request #10408 from hangxingliu/liu/fix-proxy
fix: respect proxy and timeout settings in audio-related aiohttp requests
2025-02-19 23:16:15 -08:00
星海 de8492de34 fix: GitHub OAuth email retrieval when public email is not set 2025-02-20 15:06:07 +08:00
Liu Yue 90d9cdacfa fix: respect proxy and timeout settings in audio-related aiohttp requests 2025-02-20 14:55:45 +08:00
Timothy Jaeryang Baek 13e58c1b64 Merge pull request #10360 from ferret99gt/ollama-option-conversion-fixes
fix: Update Ollama option handling in payload.py's convert_payload_openai_to_ollama
2025-02-19 22:44:07 -08:00
Timothy Jaeryang Baek d5a049dc54 Merge pull request #10401 from xinhai-ai/main
fix: email claim doesn't effect
2025-02-19 20:43:43 -08:00
星海 dec44e4bcd Fix: email claim doesn't effect 2025-02-20 11:32:48 +08:00
Timothy Jaeryang Baek ae0554224a refac 2025-02-19 18:22:31 -08:00
Timothy Jaeryang Baek fe5c4b95d5 enh: configurable jupyter execution timeout 2025-02-19 17:05:37 -08:00
Timothy J. Baek 90fca993f3 wip: jupyter code excution timeout 2025-02-19 16:41:54 -08:00
Timothy Jaeryang Baek efefff71d1 Merge pull request #10363 from ferret99gt/ollama-model-option-conversion-updates
feat: Update Payload's apply_model_params_to_body_ollama with full Ollama supported options
2025-02-19 12:32:46 -08:00
Timothy Jaeryang Baek 5da47de6eb Merge pull request #10366 from tarmst/fix-no-nested-claims-for-group-oauth
Fix: Add nested claim search for group oauth management
2025-02-19 12:23:54 -08:00
Timothy Jaeryang Baek b032f34831 Merge pull request #10364 from Lutherwaves/translations
i18n: Add bulgarian translations
2025-02-19 12:23:30 -08:00
Timothy Jaeryang Baek ea4ad3944c Merge pull request #10378 from Seniorsimo/message-conversion
**fix** Add support for tool message in openai to Ollama message conversion
2025-02-19 12:23:11 -08:00
Timothy Jaeryang Baek ea3f873ae9 Merge pull request #10377 from Seniorsimo/chat-message-validator
**fix** ChatMessage validator: content can be null when using tools
2025-02-19 12:22:31 -08:00
Timothy Jaeryang Baek de7e8fd918 Merge pull request #10362 from Seniorsimo/usage-openai-compatible
**fix** Added OpenAI usage standard keys to API signature
2025-02-19 12:20:41 -08:00
Timothy Jaeryang Baek 17560967c7 Merge pull request #10347 from Youggls/dev
fix(deps): upgrade pydantic from v2.9.2 to v2.10.3+ in requirements.txt and pyproject.toml
2025-02-19 12:18:51 -08:00
Timothy Jaeryang Baek 9a4f2b9aa3 Merge pull request #10354 from TiancongLx/dev
i18n: update zh-TW
2025-02-19 12:18:32 -08:00
Simone 6f698073bb Add support for tool message in openai to Ollama message conversion 2025-02-19 21:11:39 +01:00
Simone 8662108344 Fix on ChatMessage validator: content can be null when using tools 2025-02-19 21:03:23 +01:00
tarmst efe3bca19e Add nested claim search for groups oauth claim 2025-02-19 16:47:52 +00:00
lutherwaves 6898fd7d0f chore: Add bulgarian translations 2025-02-19 17:35:56 +02:00
ferret99gt 5701d6d333 Change the opt dictionary to a mappings dictionary with appropriate casts
This is to bring consistency with apply_model_params_to_body_openai. Both now use a mapping dictionary then call and return apply_model_params_to_body directly.
2025-02-19 09:30:16 -05:00
Simone 074ce356de Added OpenAI usagerequested keys 2025-02-19 15:28:39 +01:00
ferret99gt fa885c3346 Update remapping logic
We copy the params from from the original key to the new key, then delete it. This is to ensure Ollama only gets valid options.

(Add a comment as well)
2025-02-19 09:25:47 -05:00
ferret99gt a4249a6351 Remove remapping of frequency_penalty to repeat_penalty
1) Ollama natively supports frequency_penalty, so this is not necessary.
2) Repeat_penalty is being added to Open WebUI in PR #10016, allowing Ollama users to pick which penalty methods they want.
2025-02-19 09:24:38 -05:00
ferret99gt 877d21a029 Move parameter remapping above the opt dictionary.
This is so that any remappings are handled before options are handled by apply_model_params_to_body
2025-02-19 09:23:33 -05:00
ferret99gt 6b2ba73701 Remove tfs_z
See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8

tfs_z was removed from Llama.cpp and Ollama in January 2025.
2025-02-19 09:21:36 -05:00
ferret99gt cee08e316d Update Ollama request option list with full supported options
See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8

This adds the full suite of supported Ollama options.
2025-02-19 09:21:05 -05:00
ferret99gt 8125b0499b Remove empty ollama_options
1) The empty dictionary from line 176 is never used.
2) Lines 193-194 are not necessary as they were already done at line 177
2025-02-19 08:57:05 -05:00
ferret99gt 57b01cf8fb Fix for system prompt setting
1) Ollama supports sending the system prompt as a parameter, not as an option. (See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8) However, it is in the options dictionary and needs moved to the payload dictionary.
2) After moving the system parameter from ollama_options to ollama_payload, delete it from ollama_options. This is to prevent Ollama throwing a warning about invalid options.
2025-02-19 08:55:11 -05:00
ferret99gt fea169a9c0 Core fix for num_predict not working.
1) max_tokens was being looked for in openai_payload, but is present in openai_payload['options'], so is never found.
2) After copying the value for max_tokens to num_predict, delete max_tokens from the dictionary. This is to prevent Ollama throwing a warning about invalid option (max_tokens)
2025-02-19 08:52:34 -05:00
ferret99gt adde37394b Remove parameters that map directly, as they are part of options
1) This may be legacy code?
2) All three of these parameters, temperature, top_p and seed, are found in openai_payload["options"], not openai_payload. They do not need remapped any longer.
2025-02-19 08:44:59 -05:00
ferret99gt aea8977d05 Remove mapping of max_completion_tokens
1) max_completion_tokens is being looked for in openai_payload, but would be located in openai_payload['options'], so is never found. (This applies to the prior two commits as well).
2) max_completion_tokens is not sent from the frontend, only max_tokens. It does not appear in AdvancedParams.svelte.
2b) Openai.py does use max_completion_tokens, but for o1,o3 models and converts it from max_tokens.
2025-02-19 08:39:33 -05:00
ferret99gt e6919c3242 Remove mapping of frequency_penalty to repeat_penalty
1) Ollama natively supports frequency_penalty.
2) repeat_penaltywas added to Open Webui in PR #10016 and is not merged to main yet at this time. Once both changes go live, Ollama users can freely choose between frequency/presence penalty, or repeat penalty, as they choose.
2025-02-19 08:33:29 -05:00
ferret99gt a560f789e4 Remove mapping of presence_penalty to new_topix_penalty
1) Ollama natively supports presence_penalty.
2) new_topic_penalty is not a valid option in Ollama. (See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8)
3) Presence_penalty was added to Open Webui in PR #10016 and is not merged to main yet at this time.
2025-02-19 08:31:57 -05:00
Tiancong Li 61c6048b45 i18n: update zh-TW 2025-02-19 20:06:03 +08:00
Youggls 1692a52a6a fix(deps): upgrade pydantic from v2.9.2 to v2.10.3+ in requirements.txt and pyproject.toml 2025-02-19 19:08:17 +08:00
Timothy Jaeryang Baek 3e1337d753 Merge pull request #10341 from Youggls/dev
feat: Add Firecrawl web loader integration
2025-02-19 02:07:19 -08:00
Youggls 0fb3c08181 feat: Add Firecrawl web loader integration 2025-02-19 16:54:44 +08:00
Timothy Jaeryang Baek dab6396ebe fix: inline citations 2025-02-19 00:35:01 -08:00
Timothy Jaeryang Baek c073b8b4ee refac 2025-02-18 23:49:27 -08:00
Timothy Jaeryang Baek 8f72f8def9 fix: styling 2025-02-18 23:44:54 -08:00
Timothy Jaeryang Baek 9ca4727573 enh: web search behaviour 2025-02-18 21:29:27 -08:00
Timothy Jaeryang Baek 5465cabd40 refac 2025-02-18 21:17:09 -08:00
Timothy Jaeryang Baek 81715f6553 enh: RAG full context mode 2025-02-18 21:14:58 -08:00
Timothy Jaeryang Baek a6a7c548d5 refac: code block styling 2025-02-18 20:24:04 -08:00
Timothy Jaeryang Baek 003968f06a enh: jupyter matplotlib support 2025-02-18 20:15:16 -08:00
Timothy Jaeryang Baek 2ee09d9f7d fix: prompt suggestions settings 2025-02-18 20:07:59 -08:00
Timothy Jaeryang Baek 9380fcd09a refac 2025-02-18 20:01:24 -08:00
Timothy Jaeryang Baek 32432d6df1 fix: do NOT update changelog 2025-02-18 20:00:24 -08:00
Timothy Jaeryang Baek 1bbecd46c8 Merge pull request #10052 from roryeckel/playwright
Support Playwright RAG Web Loader: Revised
2025-02-18 19:57:48 -08:00
Timothy Jaeryang Baek c658781995 Merge pull request #10268 from hurxxxx/feat/hide-citations-in-message
feat: citations disabled model to exclude source citations in messages
2025-02-18 19:37:12 -08:00
Timothy Jaeryang Baek 84af508525 Merge branch 'dev' into feat/hide-citations-in-message 2025-02-18 19:37:03 -08:00
Timothy Jaeryang Baek 6f792e0558 Merge pull request #10271 from juquxiang/feat_user_query
Added user filtering by email and username
2025-02-18 19:36:18 -08:00
Timothy Jaeryang Baek 4ef7aff663 refac 2025-02-18 19:35:22 -08:00
Timothy Jaeryang Baek 8f7528a0bc Merge pull request #10272 from mkhludnev/dedupe-rag-docs
fix: dedupe results from multiple queries
2025-02-18 19:34:33 -08:00
Timothy Jaeryang Baek 5a7f9c81e3 Merge pull request #10308 from crpietschmann/azure-storage
feat: Add AzureStorageProvider for Azure Blob Storage support
2025-02-18 19:32:17 -08:00
Timothy Jaeryang Baek 886d756200 Merge pull request #10285 from the-c0d3br34k3r/issue8885_auth
fix: Fixed an issue with clearing application cookies during OAuth signout
2025-02-18 19:31:24 -08:00
Timothy Jaeryang Baek a33214ab6b Merge pull request #10304 from ttv20/dev
i18n: Improve RTL text handling in chat and sidebar with auto-direction #2
2025-02-18 19:31:12 -08:00
Timothy Jaeryang Baek 32a90deeaf Merge pull request #10309 from JoaoCostaIFG/gemini_image_gen
feat: add Google Imagen/Gemini API image generation
2025-02-18 19:30:20 -08:00
Timothy Jaeryang Baek 0b271d24d1 Merge pull request #10311 from alexgleason/fix-undefined-title
Fix "Cannot read properties of undefined (reading 'startsWith')"
2025-02-18 19:29:42 -08:00
Timothy Jaeryang Baek ea759c4742 Merge pull request #10323 from iamobservable/feature/frontend-authentication-redirection
feat: Feature/frontend authentication redirection
2025-02-18 19:29:23 -08:00
= 7837843f82 Add redirect capability
This feature allows the authentication process to redirect to a
route passed in the querystring. This allows the /auth route
a means of bringing the user to an expected route instead of the
main page (root).
2025-02-18 21:16:54 -06:00
Alex Gleason 5639ba423b Fix "Cannot read properties of undefined (reading 'startsWith')" 2025-02-18 18:47:56 -06:00
JoaoCostaIFG 918764a4f7 fix: Use x-goog-api-key header for Gemini image generation
Place the API key in a header instead of a query parameter. This avoids leaking the API key in logs on request failure, etc...
2025-02-19 00:00:54 +00:00
JoaoCostaIFG e56b5c063c feat: add Google Imagen/Gemini API image generation
Adds support for Gemini API as an image generation backend. By setting the API Base URL to something like 'https://generativelanguage.googleapis.com/v1beta' and providing their API Key, users should be able to start generating images using models like 'imagen-3.0-generate-002'.
2025-02-18 22:39:32 +00:00
Chris Pietschmann a232f1f34e Update test_provider.py 2025-02-18 15:53:54 -05:00
Chris Pietschmann 9864185b57 Update test_provider.py 2025-02-18 15:49:44 -05:00
Chris Pietschmann 888ae00867 Update test_provider.py 2025-02-18 15:39:47 -05:00
Chris Pietschmann 7b5f82ffc7 Update test_provider.py 2025-02-18 15:26:04 -05:00
Chris Pietschmann 7404494772 formatting 2025-02-18 15:19:35 -05:00
Chris Pietschmann a29f83c4e7 updates to formatting 2025-02-18 15:17:49 -05:00
Chris Pietschmann 4c352ff974 Update test_provider.py 2025-02-18 15:14:46 -05:00
Chris Pietschmann 55bd7a1c65 Update test_provider.py 2025-02-18 15:13:30 -05:00
Chris Pietschmann 9c8c837ab9 Update test_provider.py 2025-02-18 15:12:35 -05:00
Chris Pietschmann 2c328cc7c9 Update test_provider.py 2025-02-18 15:04:49 -05:00
Chris Pietschmann 56060db29d Update test_provider.py 2025-02-18 15:01:26 -05:00
Chris Pietschmann 7d1ec20429 Update test_provider.py 2025-02-18 14:57:51 -05:00
Chris Pietschmann ff5f0c3e39 Update test_provider.py 2025-02-18 14:51:17 -05:00
Chris Pietschmann 9a2e81f5f0 Update test_provider.py 2025-02-18 14:49:03 -05:00
Chris Pietschmann f674e28263 Update test_provider.py 2025-02-18 14:45:13 -05:00
Chris Pietschmann 4a9a88b683 Update test_provider.py 2025-02-18 14:41:42 -05:00
Chris Pietschmann b86f8df29f Update test_provider.py 2025-02-18 14:37:10 -05:00
Elkana Bardugo 61644f216e Update CHANGELOG.md 2025-02-18 21:30:25 +02:00
Chris Pietschmann cb0666ac21 update 2025-02-18 14:13:54 -05:00
Chris Pietschmann 2f4f4c2376 add tests 2025-02-18 14:09:00 -05:00
Elkana Bardugo 0b1e30988a Update MarkdownTokens.svelte
More dir="auto" to auto direction on RTL
2025-02-18 21:08:25 +02:00
Chris Pietschmann 13227fc017 Merge branch 'azure-storage' of https://github.com/crpietschmann/open-webui into azure-storage 2025-02-18 13:39:37 -05:00
Chris Pietschmann cc4598c41b Update build-release.yml 2025-02-18 13:39:27 -05:00
Timothy Jaeryang Baek e7560a5acf Merge pull request #10269 from KarlLee830/translate
i18n: Update Chinese Translation
2025-02-18 10:37:16 -08:00
Chris Pietschmann 745b24f13a Merge branch 'open-webui:main' into azure-storage 2025-02-18 13:36:56 -05:00
Chris Pietschmann aee57107bc Update config.py 2025-02-18 13:27:37 -05:00
Chris Pietschmann e4febfa097 Add AzureStorageProvider config options 2025-02-18 13:25:49 -05:00
Chris Pietschmann fd3c24af4e Add AzureStorageProvider 2025-02-18 13:25:31 -05:00
Timothy Jaeryang Baek d0114e0703 fix: temp chat issue 2025-02-18 09:57:12 -08:00
Timothy Jaeryang Baek 359f8b67f8 fix: mobile hover issue 2025-02-18 09:54:31 -08:00
Ranjan Mohan 6c6be5de88 Fixed an issue with clearing application cookies during OAuth signout
Closes #8885.

During the OAuth signout flow, although the `token` and `oauth_id_token`
cookies were marked for deletion, a new RedirectResponse is created and
returned. This does not contain the header info from the he Response object
used to mark the cookies to be deleted. Hence the cookies remained.
Fixed this by re-using the headers from the other Response object.
2025-02-18 12:44:14 -05:00
mikhail-khludnev 925bfe840b dedupe results from multiple queries 2025-02-18 20:10:57 +03:00
juxiang 55b0ac85d1 Added user filtering by email and username
Added user filtering by email and username
2025-02-19 00:53:38 +08:00
KarlLee830 d4743b1a17 i18n: Update Chinese Translation 2025-02-18 23:42:22 +08:00
KarlLee830 4939d6871b i18n: Update Chinese Translation 2025-02-18 23:40:25 +08:00
hurxxxx 9d06e3b2cd feat: chat responses using the citations-disabled model to exclude source citations 2025-02-18 21:56:16 +09:00
Timothy Jaeryang Baek 3f3a5bb0ab Merge pull request #10238 from open-webui/dev
0.5.14
2025-02-17 21:48:39 -08:00
Timothy Jaeryang Baek 0c3ba5123e doc: changelog 2025-02-17 21:48:00 -08:00
Timothy Jaeryang Baek c513308ded Merge pull request #10236 from open-webui/dev
refac
2025-02-17 21:34:36 -08:00
Timothy Jaeryang Baek 1764170307 refac 2025-02-17 21:34:06 -08:00
Rory 10e0c81de9 Merge remote-tracking branch 'upstream/dev' into playwright
# Conflicts:
#	backend/open_webui/retrieval/web/utils.py
#	backend/open_webui/routers/retrieval.py
2025-02-17 21:53:39 -06:00
Rory bc82f48ebf refac: RAG_WEB_LOADER -> RAG_WEB_LOADER_ENGINE 2025-02-17 21:43:32 -06:00
Rory 66c2acc08d Merge branch 'dev' into playwright 2025-02-15 22:14:16 -06:00
Rory 8e9b00a017 Fix docstring 2025-02-14 22:48:15 -06:00
Rory b1bab2ece8 Remove duplicate loader.alazy_load line from merge 2025-02-14 22:43:46 -06:00
Rory aa2b764d74 Finalize incomplete merge to update playwright branch
Introduced feature parity for trust_env
2025-02-14 22:32:45 -06:00
Rory 4da220c513 Merge remote-tracking branch 'upstream/dev' into playwright
# Conflicts:
#	backend/open_webui/config.py
#	backend/open_webui/main.py
#	backend/open_webui/retrieval/web/utils.py
#	backend/open_webui/routers/retrieval.py
#	backend/open_webui/utils/middleware.py
#	pyproject.toml
2025-02-14 20:48:22 -06:00
silentoplayz 1a2d81bc01 Update main.py
Change ASCII font for "Open WebUI" from "Standard" to "ANSI Shadow"
2025-02-14 12:48:33 -05:00
Didier FOURNOUT 06062568c7 black formatting 2025-02-13 16:12:46 +00:00
Didier FOURNOUT 6d62e71c34 Add x-Open-Webui headers for ollama + more for openai 2025-02-13 15:29:26 +00:00
Rory 40d4db97e6 Merge remote-tracking branch 'upstream/dev' into playwright 2025-02-12 22:32:44 -06:00
Rory 2c711d8365 Merge remote-tracking branch 'upstream/dev' into playwright
# Conflicts:
#	backend/requirements.txt
2025-02-09 23:52:21 -06:00
Mazurek Michal 35f3824932 feat: Implement Document Intelligence as Content Extraction Engine 2025-02-07 13:44:47 +01:00
Rory ec6fe9939b Merge remote-tracking branch 'upstream/dev' into playwright 2025-02-05 17:47:58 -06:00
Rory 7bac1a170d Merge remote-tracking branch 'upstream/dev' into playwright
# Conflicts:
#	backend/open_webui/retrieval/web/utils.py
2025-02-03 22:32:46 -06:00
Rory f85d7e3686 Merge branch 'dev' into playwright 2025-02-03 20:44:39 -06:00
Rory 93cc1a88ee Merge branch 'dev' into playwright 2025-02-03 19:09:26 -06:00
Rory 1b581b714f Moving code out of playwright branch 2025-02-03 18:47:26 -06:00
Rory 22746c7a3f Merge remote-tracking branch 'upstream/dev' 2025-02-03 17:28:53 -06:00
Rory f837d2cdbb Merge branch 'dev' of https://github.com/open-webui/open-webui
# Conflicts:
#	src/lib/i18n/locales/sr-RS/translation.json
2025-02-02 20:31:27 -06:00
Rory c3df481b22 Introduce docker-compose.playwright.yaml + run-compose update 2025-02-02 19:44:40 -06:00
Rory 8da33721d5 Support PLAYWRIGHT_WS_URI 2025-02-02 17:58:09 -06:00
Rory a84e488a4e Fix playwright in docker by updating unstructured 2025-02-01 22:58:28 -06:00
Rory 77ae73e659 Adjust search event messages + translations 2025-01-30 23:18:11 -06:00
Rory 2452e271cd Refine RAG_WEB_LOADER 2025-01-30 20:31:31 -06:00
Rory 8dafe3cba8 Merge branch 'dev' of https://github.com/open-webui/open-webui 2025-01-30 18:26:55 -06:00
Rory 4e8b390682 Add RAG_WEB_LOADER + Playwright mode + improve stability of search 2025-01-28 23:03:15 -06:00
158 changed files with 9016 additions and 5104 deletions

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 22
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.11
+60
View File
@@ -5,6 +5,66 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.18] - 2025-02-27
### Fixed
- **🌐 Open WebUI Now Works Over LAN in Insecure Context**: Resolved an issue preventing Open WebUI from functioning when accessed over a local network in an insecure context, ensuring seamless connectivity.
- **🔄 UI Now Reflects Deleted Connections Instantly**: Fixed an issue where deleting a connection did not update the UI in real time, ensuring accurate system state visibility.
- **🛠️ Models Now Display Correctly with ENABLE_FORWARD_USER_INFO_HEADERS**: Addressed a bug where models were not visible when ENABLE_FORWARD_USER_INFO_HEADERS was set, restoring proper model listing.
## [0.5.17] - 2025-02-27
### Added
- **🚀 Instant Document Upload with Bypass Embedding & Retrieval**: Admins can now enable "Bypass Embedding & Retrieval" in Admin Settings > Documents, significantly speeding up document uploads and ensuring full document context is retained without chunking.
- **🔎 "Stream" Hook for Real-Time Filtering**: The new "stream" hook allows dynamic real-time message filtering. Learn more in our documentation (https://docs.openwebui.com/features/plugin/functions/filter).
- **☁️ OneDrive Integration**: Early support for OneDrive storage integration has been introduced, expanding file import options.
- **📈 Enhanced Logging with Loguru**: Backend logging has been improved with Loguru, making debugging and issue tracking far more efficient.
- **⚙️ General Stability Enhancements**: Backend and frontend refactoring improves performance, ensuring a smoother and more reliable user experience.
- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages.
### Fixed
- **🔄 Reliable Model Imports from the Community Platform**: Resolved import failures, allowing seamless integration of community-shared models without errors.
- **📊 OpenAI Usage Statistics Restored**: Fixed an issue where OpenAI usage metrics were not displaying correctly, ensuring accurate tracking of usage data.
- **🗂️ Deduplication for Retrieved Documents**: Documents retrieved during searches are now intelligently deduplicated, meaning no more redundant results—helping to keep information concise and relevant.
### Changed
- **📝 "Full Context Mode" Renamed for Clarity**: The "Full Context Mode" toggle in Web Search settings is now labeled "Bypass Embedding & Retrieval" for consistency across the UI.
## [0.5.16] - 2025-02-20
### Fixed
- **🔍 Web Search Retrieval Restored**: Resolved a critical issue that broke web search retrieval by reverting deduplication changes, ensuring complete and accurate search results once again.
## [0.5.15] - 2025-02-20
### Added
- **📄 Full Context Mode for Local Document Search (RAG)**: Toggle full context mode from Admin Settings > Documents to inject entire document content into context, improving accuracy for models with large context windows—ideal for deep context understanding.
- **🌍 Smarter Web Search with Agentic Workflows**: Web searches now intelligently gather and refine multiple relevant terms, similar to RAG handling, delivering significantly better search results for more accurate information retrieval.
- **🔎 Experimental Playwright Support for Web Loader**: Web content retrieval is taken to the next level with Playwright-powered scraping for enhanced accuracy in extracted web data.
- **☁️ Experimental Azure Storage Provider**: Early-stage support for Azure Storage allows more cloud storage flexibility directly within Open WebUI.
- **📊 Improved Jupyter Code Execution with Plots**: Interactive coding now properly displays inline plots, making data visualization more seamless inside chat interactions.
- **⏳ Adjustable Execution Timeout for Jupyter Interpreter**: Customize execution timeout (default: 60s) for Jupyter-based code execution, allowing longer or more constrained execution based on your needs.
- **▶️ "Running..." Indicator for Jupyter Code Execution**: A visual indicator now appears while code execution is in progress, providing real-time status updates on ongoing computations.
- **⚙️ General Backend & Frontend Stability Enhancements**: Extensive refactoring improves reliability, performance, and overall user experience for a more seamless Open WebUI.
- **🌍 Translation Updates**: Various international translation refinements ensure better localization and a more natural user interface experience.
### Fixed
- **📱 Mobile Hover Issue Resolved**: Users can now edit responses smoothly on mobile without interference, fixing a longstanding hover issue.
- **🔄 Temporary Chat Message Duplication Fixed**: Eliminated buggy behavior where messages were being unnecessarily repeated in temporary chat mode, ensuring a smooth and consistent conversation flow.
## [0.5.14] - 2025-02-17
### Fixed
- **🔧 Critical Import Error Resolved**: Fixed a circular import issue preventing 'override_static' from being correctly imported in 'open_webui.config', ensuring smooth system initialization and stability.
## [0.5.13] - 2025-02-17 ## [0.5.13] - 2025-02-17
### Added ### Added
+130 -39
View File
@@ -9,7 +9,6 @@ from pathlib import Path
from typing import Generic, Optional, TypeVar from typing import Generic, Optional, TypeVar
from urllib.parse import urlparse from urllib.parse import urlparse
import chromadb
import requests import requests
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import JSON, Column, DateTime, Integer, func from sqlalchemy import JSON, Column, DateTime, Integer, func
@@ -44,7 +43,7 @@ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
# Function to run the alembic migrations # Function to run the alembic migrations
def run_migrations(): def run_migrations():
print("Running migrations") log.info("Running migrations")
try: try:
from alembic import command from alembic import command
from alembic.config import Config from alembic.config import Config
@@ -57,7 +56,7 @@ def run_migrations():
command.upgrade(alembic_cfg, "head") command.upgrade(alembic_cfg, "head")
except Exception as e: except Exception as e:
print(f"Error: {e}") log.exception(f"Error running migrations: {e}")
run_migrations() run_migrations()
@@ -588,20 +587,6 @@ load_oauth_providers()
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve() STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
def override_static(path: str, content: str):
# Ensure path is safe
if "/" in path or ".." in path:
log.error(f"Invalid path: {path}")
return
file_path = os.path.join(STATIC_DIR, path)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as f:
f.write(base64.b64decode(content)) # Convert Base64 back to raw binary
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
if frontend_favicon.exists(): if frontend_favicon.exists():
@@ -692,12 +677,20 @@ S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None) S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None) S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None) S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
S3_USE_ACCELERATE_ENDPOINT = (
os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "False").lower() == "true"
)
S3_ADDRESSING_STYLE = os.environ.get("S3_ADDRESSING_STYLE", None)
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None) GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get( GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
"GOOGLE_APPLICATION_CREDENTIALS_JSON", None "GOOGLE_APPLICATION_CREDENTIALS_JSON", None
) )
AZURE_STORAGE_ENDPOINT = os.environ.get("AZURE_STORAGE_ENDPOINT", None)
AZURE_STORAGE_CONTAINER_NAME = os.environ.get("AZURE_STORAGE_CONTAINER_NAME", None)
AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None)
#################################### ####################################
# File Upload DIR # File Upload DIR
#################################### ####################################
@@ -797,6 +790,9 @@ ENABLE_OPENAI_API = PersistentConfig(
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
GEMINI_API_BASE_URL = os.environ.get("GEMINI_API_BASE_URL", "")
if OPENAI_API_BASE_URL == "": if OPENAI_API_BASE_URL == "":
OPENAI_API_BASE_URL = "https://api.openai.com/v1" OPENAI_API_BASE_URL = "https://api.openai.com/v1"
@@ -1101,7 +1097,7 @@ try:
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]")) banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
banners = [BannerModel(**banner) for banner in banners] banners = [BannerModel(**banner) for banner in banners]
except Exception as e: except Exception as e:
print(f"Error loading WEBUI_BANNERS: {e}") log.exception(f"Error loading WEBUI_BANNERS: {e}")
banners = [] banners = []
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners) WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
@@ -1409,6 +1405,11 @@ CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig(
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
) )
CODE_EXECUTION_JUPYTER_TIMEOUT = PersistentConfig(
"CODE_EXECUTION_JUPYTER_TIMEOUT",
"code_execution.jupyter.timeout",
int(os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60")),
)
ENABLE_CODE_INTERPRETER = PersistentConfig( ENABLE_CODE_INTERPRETER = PersistentConfig(
"ENABLE_CODE_INTERPRETER", "ENABLE_CODE_INTERPRETER",
@@ -1464,6 +1465,17 @@ CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
), ),
) )
CODE_INTERPRETER_JUPYTER_TIMEOUT = PersistentConfig(
"CODE_INTERPRETER_JUPYTER_TIMEOUT",
"code_interpreter.jupyter.timeout",
int(
os.environ.get(
"CODE_INTERPRETER_JUPYTER_TIMEOUT",
os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60"),
)
),
)
DEFAULT_CODE_INTERPRETER_PROMPT = """ DEFAULT_CODE_INTERPRETER_PROMPT = """
#### Tools Available #### Tools Available
@@ -1489,21 +1501,27 @@ VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
# Chroma # Chroma
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE) if VECTOR_DB == "chroma":
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "") import chromadb
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "") CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "") CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
# Comma-separated list of header=value pairs CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "") CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
if CHROMA_HTTP_HEADERS: CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
CHROMA_HTTP_HEADERS = dict( CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get(
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")] "CHROMA_CLIENT_AUTH_CREDENTIALS", ""
) )
else: # Comma-separated list of header=value pairs
CHROMA_HTTP_HEADERS = None CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true" if CHROMA_HTTP_HEADERS:
CHROMA_HTTP_HEADERS = dict(
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
)
else:
CHROMA_HTTP_HEADERS = None
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2) # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
# Milvus # Milvus
@@ -1557,6 +1575,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig(
os.environ.get("GOOGLE_DRIVE_API_KEY", ""), os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
) )
ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
"ENABLE_ONEDRIVE_INTEGRATION",
"onedrive.enable",
os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true",
)
ONEDRIVE_CLIENT_ID = PersistentConfig(
"ONEDRIVE_CLIENT_ID",
"onedrive.client_id",
os.environ.get("ONEDRIVE_CLIENT_ID", ""),
)
# RAG Content Extraction # RAG Content Extraction
CONTENT_EXTRACTION_ENGINE = PersistentConfig( CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE", "CONTENT_EXTRACTION_ENGINE",
@@ -1570,6 +1600,26 @@ TIKA_SERVER_URL = PersistentConfig(
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
) )
DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
"DOCUMENT_INTELLIGENCE_ENDPOINT",
"rag.document_intelligence_endpoint",
os.getenv("DOCUMENT_INTELLIGENCE_ENDPOINT", ""),
)
DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
"DOCUMENT_INTELLIGENCE_KEY",
"rag.document_intelligence_key",
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
)
BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
"BYPASS_EMBEDDING_AND_RETRIEVAL",
"rag.bypass_embedding_and_retrieval",
os.environ.get("BYPASS_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
)
RAG_TOP_K = PersistentConfig( RAG_TOP_K = PersistentConfig(
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3")) "RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
) )
@@ -1585,6 +1635,12 @@ ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true", os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
) )
RAG_FULL_CONTEXT = PersistentConfig(
"RAG_FULL_CONTEXT",
"rag.full_context",
os.getenv("RAG_FULL_CONTEXT", "False").lower() == "true",
)
RAG_FILE_MAX_COUNT = PersistentConfig( RAG_FILE_MAX_COUNT = PersistentConfig(
"RAG_FILE_MAX_COUNT", "RAG_FILE_MAX_COUNT",
"rag.file.max_count", "rag.file.max_count",
@@ -1699,7 +1755,7 @@ Respond to the user query using the provided context, incorporating inline citat
- Respond in the same language as the user's query. - Respond in the same language as the user's query.
- If the context is unreadable or of poor quality, inform the user and provide the best possible answer. - If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
- If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding. - If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
- **Only include inline citations using [source_id] when a <source_id> tag is explicitly provided in the context.** - **Only include inline citations using [source_id] (e.g., [1], [2]) when a `<source_id>` tag is explicitly provided in the context.**
- Do not cite if the <source_id> tag is not provided in the context. - Do not cite if the <source_id> tag is not provided in the context.
- Do not use XML tags in your response. - Do not use XML tags in your response.
- Ensure citations are concise and directly related to the information provided. - Ensure citations are concise and directly related to the information provided.
@@ -1780,10 +1836,10 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
os.getenv("RAG_WEB_SEARCH_ENGINE", ""), os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
) )
RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig( BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
"RAG_WEB_SEARCH_FULL_CONTEXT", "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL",
"rag.web.search.full_context", "rag.web.search.bypass_embedding_and_retrieval",
os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true", os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
) )
# You can provide a list of your own websites to filter after performing a web search. # You can provide a list of your own websites to filter after performing a web search.
@@ -1933,10 +1989,34 @@ RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")), int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
) )
RAG_WEB_LOADER_ENGINE = PersistentConfig(
"RAG_WEB_LOADER_ENGINE",
"rag.web.loader.engine",
os.environ.get("RAG_WEB_LOADER_ENGINE", "safe_web"),
)
RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig( RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig(
"RAG_WEB_SEARCH_TRUST_ENV", "RAG_WEB_SEARCH_TRUST_ENV",
"rag.web.search.trust_env", "rag.web.search.trust_env",
os.getenv("RAG_WEB_SEARCH_TRUST_ENV", False), os.getenv("RAG_WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
)
PLAYWRIGHT_WS_URI = PersistentConfig(
"PLAYWRIGHT_WS_URI",
"rag.web.loader.engine.playwright.ws.uri",
os.environ.get("PLAYWRIGHT_WS_URI", None),
)
FIRECRAWL_API_KEY = PersistentConfig(
"FIRECRAWL_API_KEY",
"firecrawl.api_key",
os.environ.get("FIRECRAWL_API_KEY", ""),
)
FIRECRAWL_API_BASE_URL = PersistentConfig(
"FIRECRAWL_API_BASE_URL",
"firecrawl.api_url",
os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
) )
#################################### ####################################
@@ -2149,6 +2229,17 @@ IMAGES_OPENAI_API_KEY = PersistentConfig(
os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY), os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
) )
IMAGES_GEMINI_API_BASE_URL = PersistentConfig(
"IMAGES_GEMINI_API_BASE_URL",
"image_generation.gemini.api_base_url",
os.getenv("IMAGES_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL),
)
IMAGES_GEMINI_API_KEY = PersistentConfig(
"IMAGES_GEMINI_API_KEY",
"image_generation.gemini.api_key",
os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY),
)
IMAGE_SIZE = PersistentConfig( IMAGE_SIZE = PersistentConfig(
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512") "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
) )
@@ -2325,7 +2416,7 @@ LDAP_SEARCH_BASE = PersistentConfig(
LDAP_SEARCH_FILTERS = PersistentConfig( LDAP_SEARCH_FILTERS = PersistentConfig(
"LDAP_SEARCH_FILTER", "LDAP_SEARCH_FILTER",
"ldap.server.search_filter", "ldap.server.search_filter",
os.environ.get("LDAP_SEARCH_FILTER", ""), os.environ.get("LDAP_SEARCH_FILTER", os.environ.get("LDAP_SEARCH_FILTERS", "")),
) )
LDAP_USE_TLS = PersistentConfig( LDAP_USE_TLS = PersistentConfig(
+22
View File
@@ -419,3 +419,25 @@ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
if OFFLINE_MODE: if OFFLINE_MODE:
os.environ["HF_HUB_OFFLINE"] = "1" os.environ["HF_HUB_OFFLINE"] = "1"
####################################
# AUDIT LOGGING
####################################
ENABLE_AUDIT_LOGS = os.getenv("ENABLE_AUDIT_LOGS", "false").lower() == "true"
# Where to store log file
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
# Maximum size of a file before rotating into a new log file
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
# METADATA | REQUEST | REQUEST_RESPONSE
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "REQUEST_RESPONSE").upper()
try:
MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048)
except ValueError:
MAX_BODY_LOG_SIZE = 2048
# Comma separated list for urls to exclude from audit
AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split(
","
)
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
+6 -3
View File
@@ -2,6 +2,7 @@ import logging
import sys import sys
import inspect import inspect
import json import json
import asyncio
from pydantic import BaseModel from pydantic import BaseModel
from typing import AsyncGenerator, Generator, Iterator from typing import AsyncGenerator, Generator, Iterator
@@ -76,11 +77,13 @@ async def get_function_models(request):
if hasattr(function_module, "pipes"): if hasattr(function_module, "pipes"):
sub_pipes = [] sub_pipes = []
# Check if pipes is a function or a list # Handle pipes being a list, sync function, or async function
try: try:
if callable(function_module.pipes): if callable(function_module.pipes):
sub_pipes = function_module.pipes() if asyncio.iscoroutinefunction(function_module.pipes):
sub_pipes = await function_module.pipes()
else:
sub_pipes = function_module.pipes()
else: else:
sub_pipes = function_module.pipes sub_pipes = function_module.pipes
except Exception as e: except Exception as e:
+60 -6
View File
@@ -45,6 +45,9 @@ from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import Response, StreamingResponse from starlette.responses import Response, StreamingResponse
from open_webui.utils import logger
from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
from open_webui.utils.logger import start_logger
from open_webui.socket.main import ( from open_webui.socket.main import (
app as socket_app, app as socket_app,
periodic_usage_pool_cleanup, periodic_usage_pool_cleanup,
@@ -95,6 +98,7 @@ from open_webui.config import (
OLLAMA_API_CONFIGS, OLLAMA_API_CONFIGS,
# OpenAI # OpenAI
ENABLE_OPENAI_API, ENABLE_OPENAI_API,
ONEDRIVE_CLIENT_ID,
OPENAI_API_BASE_URLS, OPENAI_API_BASE_URLS,
OPENAI_API_KEYS, OPENAI_API_KEYS,
OPENAI_API_CONFIGS, OPENAI_API_CONFIGS,
@@ -106,6 +110,7 @@ from open_webui.config import (
CODE_EXECUTION_JUPYTER_AUTH, CODE_EXECUTION_JUPYTER_AUTH,
CODE_EXECUTION_JUPYTER_AUTH_TOKEN, CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
CODE_EXECUTION_JUPYTER_TIMEOUT,
ENABLE_CODE_INTERPRETER, ENABLE_CODE_INTERPRETER,
CODE_INTERPRETER_ENGINE, CODE_INTERPRETER_ENGINE,
CODE_INTERPRETER_PROMPT_TEMPLATE, CODE_INTERPRETER_PROMPT_TEMPLATE,
@@ -113,6 +118,7 @@ from open_webui.config import (
CODE_INTERPRETER_JUPYTER_AUTH, CODE_INTERPRETER_JUPYTER_AUTH,
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
CODE_INTERPRETER_JUPYTER_TIMEOUT,
# Image # Image
AUTOMATIC1111_API_AUTH, AUTOMATIC1111_API_AUTH,
AUTOMATIC1111_BASE_URL, AUTOMATIC1111_BASE_URL,
@@ -131,6 +137,8 @@ from open_webui.config import (
IMAGE_STEPS, IMAGE_STEPS,
IMAGES_OPENAI_API_BASE_URL, IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY, IMAGES_OPENAI_API_KEY,
IMAGES_GEMINI_API_BASE_URL,
IMAGES_GEMINI_API_KEY,
# Audio # Audio
AUDIO_STT_ENGINE, AUDIO_STT_ENGINE,
AUDIO_STT_MODEL, AUDIO_STT_MODEL,
@@ -145,6 +153,10 @@ from open_webui.config import (
AUDIO_TTS_VOICE, AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION, AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
PLAYWRIGHT_WS_URI,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
RAG_WEB_LOADER_ENGINE,
WHISPER_MODEL, WHISPER_MODEL,
DEEPGRAM_API_KEY, DEEPGRAM_API_KEY,
WHISPER_MODEL_AUTO_UPDATE, WHISPER_MODEL_AUTO_UPDATE,
@@ -152,6 +164,8 @@ from open_webui.config import (
# Retrieval # Retrieval
RAG_TEMPLATE, RAG_TEMPLATE,
DEFAULT_RAG_TEMPLATE, DEFAULT_RAG_TEMPLATE,
RAG_FULL_CONTEXT,
BYPASS_EMBEDDING_AND_RETRIEVAL,
RAG_EMBEDDING_MODEL, RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE, RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
@@ -171,6 +185,8 @@ from open_webui.config import (
CHUNK_SIZE, CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE, CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL, TIKA_SERVER_URL,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
RAG_TOP_K, RAG_TOP_K,
RAG_TEXT_SPLITTER, RAG_TEXT_SPLITTER,
TIKTOKEN_ENCODING_NAME, TIKTOKEN_ENCODING_NAME,
@@ -179,7 +195,7 @@ from open_webui.config import (
YOUTUBE_LOADER_PROXY_URL, YOUTUBE_LOADER_PROXY_URL,
# Retrieval (Web Search) # Retrieval (Web Search)
RAG_WEB_SEARCH_ENGINE, RAG_WEB_SEARCH_ENGINE,
RAG_WEB_SEARCH_FULL_CONTEXT, BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
RAG_WEB_SEARCH_RESULT_COUNT, RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS, RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_WEB_SEARCH_TRUST_ENV, RAG_WEB_SEARCH_TRUST_ENV,
@@ -206,11 +222,13 @@ from open_webui.config import (
GOOGLE_PSE_ENGINE_ID, GOOGLE_PSE_ENGINE_ID,
GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_CLIENT_ID,
GOOGLE_DRIVE_API_KEY, GOOGLE_DRIVE_API_KEY,
ONEDRIVE_CLIENT_ID,
ENABLE_RAG_HYBRID_SEARCH, ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH, ENABLE_RAG_LOCAL_WEB_FETCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
ENABLE_RAG_WEB_SEARCH, ENABLE_RAG_WEB_SEARCH,
ENABLE_GOOGLE_DRIVE_INTEGRATION, ENABLE_GOOGLE_DRIVE_INTEGRATION,
ENABLE_ONEDRIVE_INTEGRATION,
UPLOAD_DIR, UPLOAD_DIR,
# WebUI # WebUI
WEBUI_AUTH, WEBUI_AUTH,
@@ -289,8 +307,11 @@ from open_webui.config import (
reset_config, reset_config,
) )
from open_webui.env import ( from open_webui.env import (
AUDIT_EXCLUDED_PATHS,
AUDIT_LOG_LEVEL,
CHANGELOG, CHANGELOG,
GLOBAL_LOG_LEVEL, GLOBAL_LOG_LEVEL,
MAX_BODY_LOG_SIZE,
SAFE_MODE, SAFE_MODE,
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
VERSION, VERSION,
@@ -375,6 +396,7 @@ https://github.com/open-webui/open-webui
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
start_logger()
if RESET_CONFIG_ON_START: if RESET_CONFIG_ON_START:
reset_config() reset_config()
@@ -515,6 +537,9 @@ app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = ( app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
@@ -522,6 +547,8 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
@@ -549,10 +576,13 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
@@ -576,7 +606,11 @@ app.state.config.EXA_API_KEY = EXA_API_KEY
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
app.state.config.RAG_WEB_LOADER_ENGINE = RAG_WEB_LOADER_ENGINE
app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV
app.state.config.PLAYWRIGHT_WS_URI = PLAYWRIGHT_WS_URI
app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
app.state.EMBEDDING_FUNCTION = None app.state.EMBEDDING_FUNCTION = None
app.state.ef = None app.state.ef = None
@@ -631,6 +665,7 @@ app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH
app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
) )
app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT
app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
@@ -644,6 +679,7 @@ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = (
app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ( app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
) )
app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT
######################################## ########################################
# #
@@ -658,6 +694,9 @@ app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
@@ -859,6 +898,19 @@ app.include_router(
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
try:
audit_level = AuditLevel(AUDIT_LOG_LEVEL)
except ValueError as e:
logger.error(f"Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}")
audit_level = AuditLevel.NONE
if audit_level != AuditLevel.NONE:
app.add_middleware(
AuditLoggingMiddleware,
audit_level=audit_level,
excluded_paths=AUDIT_EXCLUDED_PATHS,
max_body_size=MAX_BODY_LOG_SIZE,
)
################################## ##################################
# #
# Chat Endpoints # Chat Endpoints
@@ -891,7 +943,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
return filtered_models return filtered_models
models = await get_all_models(request) models = await get_all_models(request, user=user)
# Filter out filter pipelines # Filter out filter pipelines
models = [ models = [
@@ -920,7 +972,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
@app.get("/api/models/base") @app.get("/api/models/base")
async def get_base_models(request: Request, user=Depends(get_admin_user)): async def get_base_models(request: Request, user=Depends(get_admin_user)):
models = await get_all_base_models(request) models = await get_all_base_models(request, user=user)
return {"data": models} return {"data": models}
@@ -931,7 +983,7 @@ async def chat_completion(
user=Depends(get_verified_user), user=Depends(get_verified_user),
): ):
if not request.app.state.MODELS: if not request.app.state.MODELS:
await get_all_models(request) await get_all_models(request, user=user)
model_item = form_data.pop("model_item", {}) model_item = form_data.pop("model_item", {})
tasks = form_data.pop("background_tasks", None) tasks = form_data.pop("background_tasks", None)
@@ -967,7 +1019,7 @@ async def chat_completion(
"files": form_data.get("files", None), "files": form_data.get("files", None),
"features": form_data.get("features", None), "features": form_data.get("features", None),
"variables": form_data.get("variables", None), "variables": form_data.get("variables", None),
"model": model_info, "model": model_info.model_dump() if model_info else model,
"direct": model_item.get("direct", False), "direct": model_item.get("direct", False),
**( **(
{"function_calling": "native"} {"function_calling": "native"}
@@ -1126,6 +1178,7 @@ async def get_app_config(request: Request):
"enable_admin_export": ENABLE_ADMIN_EXPORT, "enable_admin_export": ENABLE_ADMIN_EXPORT,
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
} }
if user is not None if user is not None
else {} else {}
@@ -1157,6 +1210,7 @@ async def get_app_config(request: Request):
"client_id": GOOGLE_DRIVE_CLIENT_ID.value, "client_id": GOOGLE_DRIVE_CLIENT_ID.value,
"api_key": GOOGLE_DRIVE_API_KEY.value, "api_key": GOOGLE_DRIVE_API_KEY.value,
}, },
"onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value},
} }
if user is not None if user is not None
else {} else {}
+9 -5
View File
@@ -1,3 +1,4 @@
import logging
import json import json
import time import time
import uuid import uuid
@@ -5,7 +6,7 @@ from typing import Optional
from open_webui.internal.db import Base, get_db from open_webui.internal.db import Base, get_db
from open_webui.models.tags import TagModel, Tag, Tags from open_webui.models.tags import TagModel, Tag, Tags
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
@@ -16,6 +17,9 @@ from sqlalchemy.sql import exists
# Chat DB Schema # Chat DB Schema
#################### ####################
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Chat(Base): class Chat(Base):
__tablename__ = "chat" __tablename__ = "chat"
@@ -670,7 +674,7 @@ class ChatTable:
# Perform pagination at the SQL level # Perform pagination at the SQL level
all_chats = query.offset(skip).limit(limit).all() all_chats = query.offset(skip).limit(limit).all()
print(len(all_chats)) log.info(f"The number of chats: {len(all_chats)}")
# Validate and return chats # Validate and return chats
return [ChatModel.model_validate(chat) for chat in all_chats] return [ChatModel.model_validate(chat) for chat in all_chats]
@@ -731,7 +735,7 @@ class ChatTable:
query = db.query(Chat).filter_by(user_id=user_id) query = db.query(Chat).filter_by(user_id=user_id)
tag_id = tag_name.replace(" ", "_").lower() tag_id = tag_name.replace(" ", "_").lower()
print(db.bind.dialect.name) log.info(f"DB dialect name: {db.bind.dialect.name}")
if db.bind.dialect.name == "sqlite": if db.bind.dialect.name == "sqlite":
# SQLite JSON1 querying for tags within the meta JSON field # SQLite JSON1 querying for tags within the meta JSON field
query = query.filter( query = query.filter(
@@ -752,7 +756,7 @@ class ChatTable:
) )
all_chats = query.all() all_chats = query.all()
print("all_chats", all_chats) log.debug(f"all_chats: {all_chats}")
return [ChatModel.model_validate(chat) for chat in all_chats] return [ChatModel.model_validate(chat) for chat in all_chats]
def add_chat_tag_by_id_and_user_id_and_tag_name( def add_chat_tag_by_id_and_user_id_and_tag_name(
@@ -810,7 +814,7 @@ class ChatTable:
count = query.count() count = query.count()
# Debugging output for inspection # Debugging output for inspection
print(f"Count of chats for tag '{tag_name}':", count) log.info(f"Count of chats for tag '{tag_name}': {count}")
return count return count
+1 -1
View File
@@ -118,7 +118,7 @@ class FeedbackTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(e) log.exception(f"Error creating a new feedback: {e}")
return None return None
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]: def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
+1 -1
View File
@@ -119,7 +119,7 @@ class FilesTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(f"Error creating tool: {e}") log.exception(f"Error inserting a new file: {e}")
return None return None
def get_file_by_id(self, id: str) -> Optional[FileModel]: def get_file_by_id(self, id: str) -> Optional[FileModel]:
+1 -1
View File
@@ -82,7 +82,7 @@ class FolderTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(e) log.exception(f"Error inserting a new folder: {e}")
return None return None
def get_folder_by_id_and_user_id( def get_folder_by_id_and_user_id(
+8 -4
View File
@@ -105,7 +105,7 @@ class FunctionsTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(f"Error creating tool: {e}") log.exception(f"Error creating a new function: {e}")
return None return None
def get_function_by_id(self, id: str) -> Optional[FunctionModel]: def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
@@ -170,7 +170,7 @@ class FunctionsTable:
function = db.get(Function, id) function = db.get(Function, id)
return function.valves if function.valves else {} return function.valves if function.valves else {}
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(f"Error getting function valves by id {id}: {e}")
return None return None
def update_function_valves_by_id( def update_function_valves_by_id(
@@ -202,7 +202,9 @@ class FunctionsTable:
return user_settings["functions"]["valves"].get(id, {}) return user_settings["functions"]["valves"].get(id, {})
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(
f"Error getting user values by id {id} and user id {user_id}: {e}"
)
return None return None
def update_user_valves_by_id_and_user_id( def update_user_valves_by_id_and_user_id(
@@ -225,7 +227,9 @@ class FunctionsTable:
return user_settings["functions"]["valves"][id] return user_settings["functions"]["valves"][id]
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
)
return None return None
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]: def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
+2 -3
View File
@@ -166,7 +166,7 @@ class ModelsTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to insert a new model: {e}")
return None return None
def get_all_models(self) -> list[ModelModel]: def get_all_models(self) -> list[ModelModel]:
@@ -246,8 +246,7 @@ class ModelsTable:
db.refresh(model) db.refresh(model)
return ModelModel.model_validate(model) return ModelModel.model_validate(model)
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to update the model by id {id}: {e}")
return None return None
def delete_model_by_id(self, id: str) -> bool: def delete_model_by_id(self, id: str) -> bool:
+1 -1
View File
@@ -61,7 +61,7 @@ class TagTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(e) log.exception(f"Error inserting a new tag: {e}")
return None return None
def get_tag_by_name_and_user_id( def get_tag_by_name_and_user_id(
+8 -4
View File
@@ -131,7 +131,7 @@ class ToolsTable:
else: else:
return None return None
except Exception as e: except Exception as e:
print(f"Error creating tool: {e}") log.exception(f"Error creating a new tool: {e}")
return None return None
def get_tool_by_id(self, id: str) -> Optional[ToolModel]: def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
@@ -175,7 +175,7 @@ class ToolsTable:
tool = db.get(Tool, id) tool = db.get(Tool, id)
return tool.valves if tool.valves else {} return tool.valves if tool.valves else {}
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(f"Error getting tool valves by id {id}: {e}")
return None return None
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]: def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
@@ -204,7 +204,9 @@ class ToolsTable:
return user_settings["tools"]["valves"].get(id, {}) return user_settings["tools"]["valves"].get(id, {})
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(
f"Error getting user values by id {id} and user_id {user_id}: {e}"
)
return None return None
def update_user_valves_by_id_and_user_id( def update_user_valves_by_id_and_user_id(
@@ -227,7 +229,9 @@ class ToolsTable:
return user_settings["tools"]["valves"][id] return user_settings["tools"]["valves"][id]
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
)
return None return None
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]: def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
@@ -4,6 +4,7 @@ import ftfy
import sys import sys
from langchain_community.document_loaders import ( from langchain_community.document_loaders import (
AzureAIDocumentIntelligenceLoader,
BSHTMLLoader, BSHTMLLoader,
CSVLoader, CSVLoader,
Docx2txtLoader, Docx2txtLoader,
@@ -76,6 +77,7 @@ known_source_ext = [
"jsx", "jsx",
"hs", "hs",
"lhs", "lhs",
"json",
] ]
@@ -147,6 +149,27 @@ class Loader:
file_path=file_path, file_path=file_path,
mime_type=file_content_type, mime_type=file_content_type,
) )
elif (
self.engine == "document_intelligence"
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
and self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != ""
and (
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
or file_content_type
in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
]
)
):
loader = AzureAIDocumentIntelligenceLoader(
file_path=file_path,
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
)
else: else:
if file_ext == "pdf": if file_ext == "pdf":
loader = PyPDFLoader( loader = PyPDFLoader(
@@ -1,13 +1,19 @@
import os import os
import logging
import torch import torch
import numpy as np import numpy as np
from colbert.infra import ColBERTConfig from colbert.infra import ColBERTConfig
from colbert.modeling.checkpoint import Checkpoint from colbert.modeling.checkpoint import Checkpoint
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ColBERT: class ColBERT:
def __init__(self, name, **kwargs) -> None: def __init__(self, name, **kwargs) -> None:
print("ColBERT: Loading model", name) log.info("ColBERT: Loading model", name)
self.device = "cuda" if torch.cuda.is_available() else "cpu" self.device = "cuda" if torch.cuda.is_available() else "cpu"
DOCKER = kwargs.get("env") == "docker" DOCKER = kwargs.get("env") == "docker"
+165 -59
View File
@@ -5,6 +5,7 @@ from typing import Optional, Union
import asyncio import asyncio
import requests import requests
import hashlib
from huggingface_hub import snapshot_download from huggingface_hub import snapshot_download
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
@@ -14,8 +15,10 @@ from langchain_core.documents import Document
from open_webui.config import VECTOR_DB from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.misc import get_last_user_message from open_webui.utils.misc import get_last_user_message, calculate_sha256_string
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
from open_webui.models.files import Files
from open_webui.env import ( from open_webui.env import (
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
@@ -80,7 +83,20 @@ def query_doc(
return result return result
except Exception as e: except Exception as e:
print(e) log.exception(f"Error querying doc {collection_name} with limit {k}: {e}")
raise e
def get_doc(collection_name: str, user: UserModel = None):
try:
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
if result:
log.info(f"query_doc:result {result.ids} {result.metadatas}")
return result
except Exception as e:
log.exception(f"Error getting doc {collection_name}: {e}")
raise e raise e
@@ -137,47 +153,80 @@ def query_doc_with_hybrid_search(
raise e raise e
def merge_and_sort_query_results( def merge_get_results(get_results: list[dict]) -> dict:
query_results: list[dict], k: int, reverse: bool = False
) -> list[dict]:
# Initialize lists to store combined data # Initialize lists to store combined data
combined_distances = []
combined_documents = [] combined_documents = []
combined_metadatas = [] combined_metadatas = []
combined_ids = []
for data in query_results: for data in get_results:
combined_distances.extend(data["distances"][0])
combined_documents.extend(data["documents"][0]) combined_documents.extend(data["documents"][0])
combined_metadatas.extend(data["metadatas"][0]) combined_metadatas.extend(data["metadatas"][0])
combined_ids.extend(data["ids"][0])
# Create a list of tuples (distance, document, metadata) # Create the output dictionary
combined = list(zip(combined_distances, combined_documents, combined_metadatas)) result = {
"documents": [combined_documents],
"metadatas": [combined_metadatas],
"ids": [combined_ids],
}
return result
def merge_and_sort_query_results(
query_results: list[dict], k: int, reverse: bool = False
) -> dict:
# Initialize lists to store combined data
combined = []
seen_hashes = set() # To store unique document hashes
for data in query_results:
distances = data["distances"][0]
documents = data["documents"][0]
metadatas = data["metadatas"][0]
for distance, document, metadata in zip(distances, documents, metadatas):
if isinstance(document, str):
doc_hash = hashlib.md5(
document.encode()
).hexdigest() # Compute a hash for uniqueness
if doc_hash not in seen_hashes:
seen_hashes.add(doc_hash)
combined.append((distance, document, metadata))
# Sort the list based on distances # Sort the list based on distances
combined.sort(key=lambda x: x[0], reverse=reverse) combined.sort(key=lambda x: x[0], reverse=reverse)
# We don't have anything :-( # Slice to keep only the top k elements
if not combined: sorted_distances, sorted_documents, sorted_metadatas = (
sorted_distances = [] zip(*combined[:k]) if combined else ([], [], [])
sorted_documents = [] )
sorted_metadatas = []
else:
# Unzip the sorted list
sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
# Slicing the lists to include only k elements # Create and return the output dictionary
sorted_distances = list(sorted_distances)[:k] return {
sorted_documents = list(sorted_documents)[:k] "distances": [list(sorted_distances)],
sorted_metadatas = list(sorted_metadatas)[:k] "documents": [list(sorted_documents)],
"metadatas": [list(sorted_metadatas)],
# Create the output dictionary
result = {
"distances": [sorted_distances],
"documents": [sorted_documents],
"metadatas": [sorted_metadatas],
} }
return result
def get_all_items_from_collections(collection_names: list[str]) -> dict:
results = []
for collection_name in collection_names:
if collection_name:
try:
result = get_doc(collection_name=collection_name)
if result is not None:
results.append(result.model_dump())
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
else:
pass
return merge_get_results(results)
def query_collection( def query_collection(
@@ -290,6 +339,7 @@ def get_embedding_function(
def get_sources_from_files( def get_sources_from_files(
request,
files, files,
queries, queries,
embedding_function, embedding_function,
@@ -297,26 +347,74 @@ def get_sources_from_files(
reranking_function, reranking_function,
r, r,
hybrid_search, hybrid_search,
full_context=False,
): ):
log.debug(f"files: {files} {queries} {embedding_function} {reranking_function}") log.debug(
f"files: {files} {queries} {embedding_function} {reranking_function} {full_context}"
)
extracted_collections = [] extracted_collections = []
relevant_contexts = [] relevant_contexts = []
for file in files: for file in files:
context = None
if file.get("docs"): if file.get("docs"):
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
context = { context = {
"documents": [[doc.get("content") for doc in file.get("docs")]], "documents": [[doc.get("content") for doc in file.get("docs")]],
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]], "metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
} }
elif file.get("context") == "full": elif file.get("context") == "full":
# Manual Full Mode Toggle
context = { context = {
"documents": [[file.get("file").get("data", {}).get("content")]], "documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]], "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
} }
else: elif (
context = None file.get("type") != "web_search"
and request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
):
# BYPASS_EMBEDDING_AND_RETRIEVAL
if file.get("type") == "collection":
file_ids = file.get("data", {}).get("file_ids", [])
documents = []
metadatas = []
for file_id in file_ids:
file_object = Files.get_file_by_id(file_id)
if file_object:
documents.append(file_object.data.get("content", ""))
metadatas.append(
{
"file_id": file_id,
"name": file_object.filename,
"source": file_object.filename,
}
)
context = {
"documents": [documents],
"metadatas": [metadatas],
}
elif file.get("id"):
file_object = Files.get_file_by_id(file.get("id"))
if file_object:
context = {
"documents": [[file_object.data.get("content", "")]],
"metadatas": [
[
{
"file_id": file.get("id"),
"name": file_object.filename,
"source": file_object.filename,
}
]
],
}
else:
collection_names = [] collection_names = []
if file.get("type") == "collection": if file.get("type") == "collection":
if file.get("legacy"): if file.get("legacy"):
@@ -336,42 +434,50 @@ def get_sources_from_files(
log.debug(f"skipping {file} as it has already been extracted") log.debug(f"skipping {file} as it has already been extracted")
continue continue
try: if full_context:
context = None try:
if file.get("type") == "text": context = get_all_items_from_collections(collection_names)
context = file["content"] except Exception as e:
else: log.exception(e)
if hybrid_search:
try: else:
context = query_collection_with_hybrid_search( try:
context = None
if file.get("type") == "text":
context = file["content"]
else:
if hybrid_search:
try:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
queries=queries,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
except Exception as e:
log.debug(
"Error when using hybrid search, using"
" non hybrid search as fallback."
)
if (not hybrid_search) or (context is None):
context = query_collection(
collection_names=collection_names, collection_names=collection_names,
queries=queries, queries=queries,
embedding_function=embedding_function, embedding_function=embedding_function,
k=k, k=k,
reranking_function=reranking_function,
r=r,
) )
except Exception as e: except Exception as e:
log.debug( log.exception(e)
"Error when using hybrid search, using"
" non hybrid search as fallback."
)
if (not hybrid_search) or (context is None):
context = query_collection(
collection_names=collection_names,
queries=queries,
embedding_function=embedding_function,
k=k,
)
except Exception as e:
log.exception(e)
extracted_collections.extend(collection_names) extracted_collections.extend(collection_names)
if context: if context:
if "data" in file: if "data" in file:
del file["data"] del file["data"]
relevant_contexts.append({**context, "file": file}) relevant_contexts.append({**context, "file": file})
sources = [] sources = []
@@ -468,7 +574,7 @@ def generate_openai_batch_embeddings(
else: else:
raise "Something went wrong :/" raise "Something went wrong :/"
except Exception as e: except Exception as e:
print(e) log.exception(f"Error generating openai batch embeddings: {e}")
return None return None
@@ -502,7 +608,7 @@ def generate_ollama_batch_embeddings(
else: else:
raise "Something went wrong :/" raise "Something went wrong :/"
except Exception as e: except Exception as e:
print(e) log.exception(f"Error generating ollama batch embeddings: {e}")
return None return None
+6 -2
View File
@@ -1,4 +1,5 @@
import chromadb import chromadb
import logging
from chromadb import Settings from chromadb import Settings
from chromadb.utils.batch_utils import create_batches from chromadb.utils.batch_utils import create_batches
@@ -16,6 +17,10 @@ from open_webui.config import (
CHROMA_CLIENT_AUTH_PROVIDER, CHROMA_CLIENT_AUTH_PROVIDER,
CHROMA_CLIENT_AUTH_CREDENTIALS, CHROMA_CLIENT_AUTH_CREDENTIALS,
) )
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ChromaClient: class ChromaClient:
@@ -102,8 +107,7 @@ class ChromaClient:
} }
) )
return None return None
except Exception as e: except:
print(e)
return None return None
def get(self, collection_name: str) -> Optional[GetResult]: def get(self, collection_name: str) -> Optional[GetResult]:
@@ -1,7 +1,7 @@
from pymilvus import MilvusClient as Client from pymilvus import MilvusClient as Client
from pymilvus import FieldSchema, DataType from pymilvus import FieldSchema, DataType
import json import json
import logging
from typing import Optional from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
@@ -10,6 +10,10 @@ from open_webui.config import (
MILVUS_DB, MILVUS_DB,
MILVUS_TOKEN, MILVUS_TOKEN,
) )
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MilvusClient: class MilvusClient:
@@ -168,7 +172,7 @@ class MilvusClient:
try: try:
# Loop until there are no more items to fetch or the desired limit is reached # Loop until there are no more items to fetch or the desired limit is reached
while remaining > 0: while remaining > 0:
print("remaining", remaining) log.info(f"remaining: {remaining}")
current_fetch = min( current_fetch = min(
max_limit, remaining max_limit, remaining
) # Determine how many items to fetch in this iteration ) # Determine how many items to fetch in this iteration
@@ -195,10 +199,12 @@ class MilvusClient:
if results_count < current_fetch: if results_count < current_fetch:
break break
print(all_results) log.debug(all_results)
return self._result_to_get_result([all_results]) return self._result_to_get_result([all_results])
except Exception as e: except Exception as e:
print(e) log.exception(
f"Error querying collection {collection_name} with limit {limit}: {e}"
)
return None return None
def get(self, collection_name: str) -> Optional[GetResult]: def get(self, collection_name: str) -> Optional[GetResult]:
@@ -1,4 +1,5 @@
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
import logging
from sqlalchemy import ( from sqlalchemy import (
cast, cast,
column, column,
@@ -24,9 +25,14 @@ from sqlalchemy.exc import NoSuchTableError
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
from open_webui.env import SRC_LOG_LEVELS
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
Base = declarative_base() Base = declarative_base()
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class DocumentChunk(Base): class DocumentChunk(Base):
__tablename__ = "document_chunk" __tablename__ = "document_chunk"
@@ -82,10 +88,10 @@ class PgvectorClient:
) )
) )
self.session.commit() self.session.commit()
print("Initialization complete.") log.info("Initialization complete.")
except Exception as e: except Exception as e:
self.session.rollback() self.session.rollback()
print(f"Error during initialization: {e}") log.exception(f"Error during initialization: {e}")
raise raise
def check_vector_length(self) -> None: def check_vector_length(self) -> None:
@@ -150,12 +156,12 @@ class PgvectorClient:
new_items.append(new_chunk) new_items.append(new_chunk)
self.session.bulk_save_objects(new_items) self.session.bulk_save_objects(new_items)
self.session.commit() self.session.commit()
print( log.info(
f"Inserted {len(new_items)} items into collection '{collection_name}'." f"Inserted {len(new_items)} items into collection '{collection_name}'."
) )
except Exception as e: except Exception as e:
self.session.rollback() self.session.rollback()
print(f"Error during insert: {e}") log.exception(f"Error during insert: {e}")
raise raise
def upsert(self, collection_name: str, items: List[VectorItem]) -> None: def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
@@ -184,10 +190,12 @@ class PgvectorClient:
) )
self.session.add(new_chunk) self.session.add(new_chunk)
self.session.commit() self.session.commit()
print(f"Upserted {len(items)} items into collection '{collection_name}'.") log.info(
f"Upserted {len(items)} items into collection '{collection_name}'."
)
except Exception as e: except Exception as e:
self.session.rollback() self.session.rollback()
print(f"Error during upsert: {e}") log.exception(f"Error during upsert: {e}")
raise raise
def search( def search(
@@ -278,7 +286,7 @@ class PgvectorClient:
ids=ids, distances=distances, documents=documents, metadatas=metadatas ids=ids, distances=distances, documents=documents, metadatas=metadatas
) )
except Exception as e: except Exception as e:
print(f"Error during search: {e}") log.exception(f"Error during search: {e}")
return None return None
def query( def query(
@@ -310,7 +318,7 @@ class PgvectorClient:
metadatas=metadatas, metadatas=metadatas,
) )
except Exception as e: except Exception as e:
print(f"Error during query: {e}") log.exception(f"Error during query: {e}")
return None return None
def get( def get(
@@ -334,7 +342,7 @@ class PgvectorClient:
return GetResult(ids=ids, documents=documents, metadatas=metadatas) return GetResult(ids=ids, documents=documents, metadatas=metadatas)
except Exception as e: except Exception as e:
print(f"Error during get: {e}") log.exception(f"Error during get: {e}")
return None return None
def delete( def delete(
@@ -356,22 +364,22 @@ class PgvectorClient:
) )
deleted = query.delete(synchronize_session=False) deleted = query.delete(synchronize_session=False)
self.session.commit() self.session.commit()
print(f"Deleted {deleted} items from collection '{collection_name}'.") log.info(f"Deleted {deleted} items from collection '{collection_name}'.")
except Exception as e: except Exception as e:
self.session.rollback() self.session.rollback()
print(f"Error during delete: {e}") log.exception(f"Error during delete: {e}")
raise raise
def reset(self) -> None: def reset(self) -> None:
try: try:
deleted = self.session.query(DocumentChunk).delete() deleted = self.session.query(DocumentChunk).delete()
self.session.commit() self.session.commit()
print( log.info(
f"Reset complete. Deleted {deleted} items from 'document_chunk' table." f"Reset complete. Deleted {deleted} items from 'document_chunk' table."
) )
except Exception as e: except Exception as e:
self.session.rollback() self.session.rollback()
print(f"Error during reset: {e}") log.exception(f"Error during reset: {e}")
raise raise
def close(self) -> None: def close(self) -> None:
@@ -387,9 +395,9 @@ class PgvectorClient:
) )
return exists return exists
except Exception as e: except Exception as e:
print(f"Error checking collection existence: {e}") log.exception(f"Error checking collection existence: {e}")
return False return False
def delete_collection(self, collection_name: str) -> None: def delete_collection(self, collection_name: str) -> None:
self.delete(collection_name) self.delete(collection_name)
print(f"Collection '{collection_name}' deleted.") log.info(f"Collection '{collection_name}' deleted.")
@@ -1,4 +1,5 @@
from typing import Optional from typing import Optional
import logging
from qdrant_client import QdrantClient as Qclient from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.models import PointStruct from qdrant_client.http.models import PointStruct
@@ -6,9 +7,13 @@ from qdrant_client.models import models
from open_webui.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 from open_webui.config import QDRANT_URI, QDRANT_API_KEY
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 999999999 NO_LIMIT = 999999999
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient: class QdrantClient:
def __init__(self): def __init__(self):
@@ -49,7 +54,7 @@ class QdrantClient:
), ),
) )
print(f"collection {collection_name_with_prefix} successfully created!") log.info(f"collection {collection_name_with_prefix} successfully created!")
def _create_collection_if_not_exists(self, collection_name, dimension): def _create_collection_if_not_exists(self, collection_name, dimension):
if not self.has_collection(collection_name=collection_name): if not self.has_collection(collection_name=collection_name):
@@ -120,7 +125,7 @@ class QdrantClient:
) )
return self._result_to_get_result(points.points) return self._result_to_get_result(points.points)
except Exception as e: except Exception as e:
print(e) log.exception(f"Error querying a collection '{collection_name}': {e}")
return None return None
def get(self, collection_name: str) -> Optional[GetResult]: def get(self, collection_name: str) -> Optional[GetResult]:
+374 -33
View File
@@ -1,22 +1,38 @@
import socket
import aiohttp
import asyncio import asyncio
import urllib.parse
import validators
from typing import Any, AsyncIterator, Dict, Iterator, List, Sequence, Union
from langchain_community.document_loaders import (
WebBaseLoader,
)
from langchain_core.documents import Document
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import ENABLE_RAG_LOCAL_WEB_FETCH
from open_webui.env import SRC_LOG_LEVELS
import logging import logging
import socket
import ssl
import urllib.parse
import urllib.request
from collections import defaultdict
from datetime import datetime, time, timedelta
from typing import (
Any,
AsyncIterator,
Dict,
Iterator,
List,
Optional,
Sequence,
Union,
Literal,
)
import aiohttp
import certifi
import validators
from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader
from langchain_community.document_loaders.firecrawl import FireCrawlLoader
from langchain_community.document_loaders.base import BaseLoader
from langchain_core.documents import Document
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
PLAYWRIGHT_WS_URI,
RAG_WEB_LOADER_ENGINE,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
)
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"]) log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -68,6 +84,314 @@ def resolve_hostname(hostname):
return ipv4_addresses, ipv6_addresses return ipv4_addresses, ipv6_addresses
def extract_metadata(soup, url):
metadata = {"source": url}
if title := soup.find("title"):
metadata["title"] = title.get_text()
if description := soup.find("meta", attrs={"name": "description"}):
metadata["description"] = description.get("content", "No description found.")
if html := soup.find("html"):
metadata["language"] = html.get("lang", "No language found.")
return metadata
def verify_ssl_cert(url: str) -> bool:
"""Verify SSL certificate for the given URL."""
if not url.startswith("https://"):
return True
try:
hostname = url.split("://")[-1].split("/")[0]
context = ssl.create_default_context(cafile=certifi.where())
with context.wrap_socket(ssl.socket(), server_hostname=hostname) as s:
s.connect((hostname, 443))
return True
except ssl.SSLError:
return False
except Exception as e:
log.warning(f"SSL verification failed for {url}: {str(e)}")
return False
class SafeFireCrawlLoader(BaseLoader):
def __init__(
self,
web_paths,
verify_ssl: bool = True,
trust_env: bool = False,
requests_per_second: Optional[float] = None,
continue_on_failure: bool = True,
api_key: Optional[str] = None,
api_url: Optional[str] = None,
mode: Literal["crawl", "scrape", "map"] = "crawl",
proxy: Optional[Dict[str, str]] = None,
params: Optional[Dict] = None,
):
"""Concurrent document loader for FireCrawl operations.
Executes multiple FireCrawlLoader instances concurrently using thread pooling
to improve bulk processing efficiency.
Args:
web_paths: List of URLs/paths to process.
verify_ssl: If True, verify SSL certificates.
trust_env: If True, use proxy settings from environment variables.
requests_per_second: Number of requests per second to limit to.
continue_on_failure (bool): If True, continue loading other URLs on failure.
api_key: API key for FireCrawl service. Defaults to None
(uses FIRE_CRAWL_API_KEY environment variable if not provided).
api_url: Base URL for FireCrawl API. Defaults to official API endpoint.
mode: Operation mode selection:
- 'crawl': Website crawling mode (default)
- 'scrape': Direct page scraping
- 'map': Site map generation
proxy: Proxy override settings for the FireCrawl API.
params: The parameters to pass to the Firecrawl API.
Examples include crawlerOptions.
For more details, visit: https://github.com/mendableai/firecrawl-py
"""
proxy_server = proxy.get("server") if proxy else None
if trust_env and not proxy_server:
env_proxies = urllib.request.getproxies()
env_proxy_server = env_proxies.get("https") or env_proxies.get("http")
if env_proxy_server:
if proxy:
proxy["server"] = env_proxy_server
else:
proxy = {"server": env_proxy_server}
self.web_paths = web_paths
self.verify_ssl = verify_ssl
self.requests_per_second = requests_per_second
self.last_request_time = None
self.trust_env = trust_env
self.continue_on_failure = continue_on_failure
self.api_key = api_key
self.api_url = api_url
self.mode = mode
self.params = params
def lazy_load(self) -> Iterator[Document]:
"""Load documents concurrently using FireCrawl."""
for url in self.web_paths:
try:
self._safe_process_url_sync(url)
loader = FireCrawlLoader(
url=url,
api_key=self.api_key,
api_url=self.api_url,
mode=self.mode,
params=self.params,
)
yield from loader.lazy_load()
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
continue
raise e
async def alazy_load(self):
"""Async version of lazy_load."""
for url in self.web_paths:
try:
await self._safe_process_url(url)
loader = FireCrawlLoader(
url=url,
api_key=self.api_key,
api_url=self.api_url,
mode=self.mode,
params=self.params,
)
async for document in loader.alazy_load():
yield document
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
continue
raise e
def _verify_ssl_cert(self, url: str) -> bool:
return verify_ssl_cert(url)
async def _wait_for_rate_limit(self):
"""Wait to respect the rate limit if specified."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
await asyncio.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
def _sync_wait_for_rate_limit(self):
"""Synchronous version of rate limit wait."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
time.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
async def _safe_process_url(self, url: str) -> bool:
"""Perform safety checks before processing a URL."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
await self._wait_for_rate_limit()
return True
def _safe_process_url_sync(self, url: str) -> bool:
"""Synchronous version of safety checks."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
self._sync_wait_for_rate_limit()
return True
class SafePlaywrightURLLoader(PlaywrightURLLoader):
"""Load HTML pages safely with Playwright, supporting SSL verification, rate limiting, and remote browser connection.
Attributes:
web_paths (List[str]): List of URLs to load.
verify_ssl (bool): If True, verify SSL certificates.
trust_env (bool): If True, use proxy settings from environment variables.
requests_per_second (Optional[float]): Number of requests per second to limit to.
continue_on_failure (bool): If True, continue loading other URLs on failure.
headless (bool): If True, the browser will run in headless mode.
proxy (dict): Proxy override settings for the Playwright session.
playwright_ws_url (Optional[str]): WebSocket endpoint URI for remote browser connection.
"""
def __init__(
self,
web_paths: List[str],
verify_ssl: bool = True,
trust_env: bool = False,
requests_per_second: Optional[float] = None,
continue_on_failure: bool = True,
headless: bool = True,
remove_selectors: Optional[List[str]] = None,
proxy: Optional[Dict[str, str]] = None,
playwright_ws_url: Optional[str] = None,
):
"""Initialize with additional safety parameters and remote browser support."""
proxy_server = proxy.get("server") if proxy else None
if trust_env and not proxy_server:
env_proxies = urllib.request.getproxies()
env_proxy_server = env_proxies.get("https") or env_proxies.get("http")
if env_proxy_server:
if proxy:
proxy["server"] = env_proxy_server
else:
proxy = {"server": env_proxy_server}
# We'll set headless to False if using playwright_ws_url since it's handled by the remote browser
super().__init__(
urls=web_paths,
continue_on_failure=continue_on_failure,
headless=headless if playwright_ws_url is None else False,
remove_selectors=remove_selectors,
proxy=proxy,
)
self.verify_ssl = verify_ssl
self.requests_per_second = requests_per_second
self.last_request_time = None
self.playwright_ws_url = playwright_ws_url
self.trust_env = trust_env
def lazy_load(self) -> Iterator[Document]:
"""Safely load URLs synchronously with support for remote browser."""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Use remote browser if ws_endpoint is provided, otherwise use local browser
if self.playwright_ws_url:
browser = p.chromium.connect(self.playwright_ws_url)
else:
browser = p.chromium.launch(headless=self.headless, proxy=self.proxy)
for url in self.urls:
try:
self._safe_process_url_sync(url)
page = browser.new_page()
response = page.goto(url)
if response is None:
raise ValueError(f"page.goto() returned None for url {url}")
text = self.evaluator.evaluate(page, browser, response)
metadata = {"source": url}
yield Document(page_content=text, metadata=metadata)
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
continue
raise e
browser.close()
async def alazy_load(self) -> AsyncIterator[Document]:
"""Safely load URLs asynchronously with support for remote browser."""
from playwright.async_api import async_playwright
async with async_playwright() as p:
# Use remote browser if ws_endpoint is provided, otherwise use local browser
if self.playwright_ws_url:
browser = await p.chromium.connect(self.playwright_ws_url)
else:
browser = await p.chromium.launch(
headless=self.headless, proxy=self.proxy
)
for url in self.urls:
try:
await self._safe_process_url(url)
page = await browser.new_page()
response = await page.goto(url)
if response is None:
raise ValueError(f"page.goto() returned None for url {url}")
text = await self.evaluator.evaluate_async(page, browser, response)
metadata = {"source": url}
yield Document(page_content=text, metadata=metadata)
except Exception as e:
if self.continue_on_failure:
log.exception(e, "Error loading %s", url)
continue
raise e
await browser.close()
def _verify_ssl_cert(self, url: str) -> bool:
return verify_ssl_cert(url)
async def _wait_for_rate_limit(self):
"""Wait to respect the rate limit if specified."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
await asyncio.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
def _sync_wait_for_rate_limit(self):
"""Synchronous version of rate limit wait."""
if self.requests_per_second and self.last_request_time:
min_interval = timedelta(seconds=1.0 / self.requests_per_second)
time_since_last = datetime.now() - self.last_request_time
if time_since_last < min_interval:
time.sleep((min_interval - time_since_last).total_seconds())
self.last_request_time = datetime.now()
async def _safe_process_url(self, url: str) -> bool:
"""Perform safety checks before processing a URL."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
await self._wait_for_rate_limit()
return True
def _safe_process_url_sync(self, url: str) -> bool:
"""Synchronous version of safety checks."""
if self.verify_ssl and not self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
self._sync_wait_for_rate_limit()
return True
class SafeWebBaseLoader(WebBaseLoader): class SafeWebBaseLoader(WebBaseLoader):
"""WebBaseLoader with enhanced error handling for URLs.""" """WebBaseLoader with enhanced error handling for URLs."""
@@ -143,20 +467,12 @@ class SafeWebBaseLoader(WebBaseLoader):
text = soup.get_text(**self.bs_get_text_kwargs) text = soup.get_text(**self.bs_get_text_kwargs)
# Build metadata # Build metadata
metadata = {"source": path} metadata = extract_metadata(soup, path)
if title := soup.find("title"):
metadata["title"] = title.get_text()
if description := soup.find("meta", attrs={"name": "description"}):
metadata["description"] = description.get(
"content", "No description found."
)
if html := soup.find("html"):
metadata["language"] = html.get("lang", "No language found.")
yield Document(page_content=text, metadata=metadata) yield Document(page_content=text, metadata=metadata)
except Exception as e: except Exception as e:
# Log the error and continue with the next URL # Log the error and continue with the next URL
log.error(f"Error loading {path}: {e}") log.exception(e, "Error loading %s", path)
async def alazy_load(self) -> AsyncIterator[Document]: async def alazy_load(self) -> AsyncIterator[Document]:
"""Async lazy load text from the url(s) in web_path.""" """Async lazy load text from the url(s) in web_path."""
@@ -179,6 +495,12 @@ class SafeWebBaseLoader(WebBaseLoader):
return [document async for document in self.alazy_load()] return [document async for document in self.alazy_load()]
RAG_WEB_LOADER_ENGINES = defaultdict(lambda: SafeWebBaseLoader)
RAG_WEB_LOADER_ENGINES["playwright"] = SafePlaywrightURLLoader
RAG_WEB_LOADER_ENGINES["safe_web"] = SafeWebBaseLoader
RAG_WEB_LOADER_ENGINES["firecrawl"] = SafeFireCrawlLoader
def get_web_loader( def get_web_loader(
urls: Union[str, Sequence[str]], urls: Union[str, Sequence[str]],
verify_ssl: bool = True, verify_ssl: bool = True,
@@ -188,10 +510,29 @@ def get_web_loader(
# Check if the URLs are valid # Check if the URLs are valid
safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls) safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls)
return SafeWebBaseLoader( web_loader_args = {
web_path=safe_urls, "web_paths": safe_urls,
verify_ssl=verify_ssl, "verify_ssl": verify_ssl,
requests_per_second=requests_per_second, "requests_per_second": requests_per_second,
continue_on_failure=True, "continue_on_failure": True,
trust_env=trust_env, "trust_env": trust_env,
}
if PLAYWRIGHT_WS_URI.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URI.value
if RAG_WEB_LOADER_ENGINE.value == "firecrawl":
web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
# Create the appropriate WebLoader based on the configuration
WebLoaderClass = RAG_WEB_LOADER_ENGINES[RAG_WEB_LOADER_ENGINE.value]
web_loader = WebLoaderClass(**web_loader_args)
log.debug(
"Using RAG_WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
) )
return web_loader
+63 -16
View File
@@ -37,6 +37,7 @@ from open_webui.config import (
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ( from open_webui.env import (
AIOHTTP_CLIENT_TIMEOUT,
ENV, ENV,
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
DEVICE_TYPE, DEVICE_TYPE,
@@ -70,7 +71,7 @@ from pydub.utils import mediainfo
def is_mp4_audio(file_path): def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file.""" """Check if the given file is an MP4 audio file."""
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
print(f"File not found: {file_path}") log.error(f"File not found: {file_path}")
return False return False
info = mediainfo(file_path) info = mediainfo(file_path)
@@ -87,7 +88,7 @@ def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format.""" """Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4") audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav") audio.export(output_path, format="wav")
print(f"Converted {file_path} to {output_path}") log.info(f"Converted {file_path} to {output_path}")
def set_faster_whisper_model(model: str, auto_update: bool = False): def set_faster_whisper_model(model: str, auto_update: bool = False):
@@ -265,8 +266,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
payload["model"] = request.app.state.config.TTS_MODEL payload["model"] = request.app.state.config.TTS_MODEL
try: try:
# print(payload) timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
) as session:
async with session.post( async with session.post(
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
json=payload, json=payload,
@@ -323,7 +326,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
) )
try: try:
async with aiohttp.ClientSession() as session: timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
) as session:
async with session.post( async with session.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}", f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
json={ json={
@@ -380,7 +386,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}"> data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
<voice name="{language}">{payload["input"]}</voice> <voice name="{language}">{payload["input"]}</voice>
</speak>""" </speak>"""
async with aiohttp.ClientSession() as session: timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
) as session:
async with session.post( async with session.post(
f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1", f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1",
headers={ headers={
@@ -458,7 +467,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
def transcribe(request: Request, file_path): def transcribe(request: Request, file_path):
print("transcribe", file_path) log.info(f"transcribe: {file_path}")
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
id = filename.split(".")[0] id = filename.split(".")[0]
@@ -670,7 +679,22 @@ def transcription(
def get_available_models(request: Request) -> list[dict]: def get_available_models(request: Request) -> list[dict]:
available_models = [] available_models = []
if request.app.state.config.TTS_ENGINE == "openai": if request.app.state.config.TTS_ENGINE == "openai":
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}] # Use custom endpoint if not using the official OpenAI API URL
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith(
"https://api.openai.com"
):
try:
response = requests.get(
f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models"
)
response.raise_for_status()
data = response.json()
available_models = data.get("models", [])
except Exception as e:
log.error(f"Error fetching models from custom endpoint: {str(e)}")
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
else:
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
elif request.app.state.config.TTS_ENGINE == "elevenlabs": elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try: try:
response = requests.get( response = requests.get(
@@ -701,14 +725,37 @@ def get_available_voices(request) -> dict:
"""Returns {voice_id: voice_name} dict""" """Returns {voice_id: voice_name} dict"""
available_voices = {} available_voices = {}
if request.app.state.config.TTS_ENGINE == "openai": if request.app.state.config.TTS_ENGINE == "openai":
available_voices = { # Use custom endpoint if not using the official OpenAI API URL
"alloy": "alloy", if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith(
"echo": "echo", "https://api.openai.com"
"fable": "fable", ):
"onyx": "onyx", try:
"nova": "nova", response = requests.get(
"shimmer": "shimmer", f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices"
} )
response.raise_for_status()
data = response.json()
voices_list = data.get("voices", [])
available_voices = {voice["id"]: voice["name"] for voice in voices_list}
except Exception as e:
log.error(f"Error fetching voices from custom endpoint: {str(e)}")
available_voices = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
else:
available_voices = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
elif request.app.state.config.TTS_ENGINE == "elevenlabs": elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try: try:
available_voices = get_elevenlabs_voices( available_voices = get_elevenlabs_voices(
+8 -21
View File
@@ -31,10 +31,7 @@ from open_webui.env import (
) )
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response from fastapi.responses import RedirectResponse, Response
from open_webui.config import ( from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
OPENID_PROVIDER_URL,
ENABLE_OAUTH_SIGNUP,
)
from pydantic import BaseModel from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import ( from open_webui.utils.auth import (
@@ -51,8 +48,10 @@ from open_webui.utils.access_control import get_permissions
from typing import Optional, List from typing import Optional, List
from ssl import CERT_REQUIRED, PROTOCOL_TLS from ssl import CERT_REQUIRED, PROTOCOL_TLS
from ldap3 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars if ENABLE_LDAP.value:
from ldap3 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars
router = APIRouter() router = APIRouter()
@@ -252,14 +251,6 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
if not user: if not user:
try: try:
user_count = Users.get_num_users() user_count = Users.get_num_users()
if (
request.app.state.USER_COUNT
and user_count >= request.app.state.USER_COUNT
):
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
role = ( role = (
"admin" "admin"
@@ -439,11 +430,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
) )
user_count = Users.get_num_users() user_count = Users.get_num_users()
if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
if not validate_email_format(form_data.email.lower()): if not validate_email_format(form_data.email.lower()):
raise HTTPException( raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
@@ -546,7 +532,8 @@ async def signout(request: Request, response: Response):
if logout_url: if logout_url:
response.delete_cookie("oauth_id_token") response.delete_cookie("oauth_id_token")
return RedirectResponse( return RedirectResponse(
url=f"{logout_url}?id_token_hint={oauth_id_token}" headers=response.headers,
url=f"{logout_url}?id_token_hint={oauth_id_token}",
) )
else: else:
raise HTTPException( raise HTTPException(
@@ -612,7 +599,7 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)):
admin_email = request.app.state.config.ADMIN_EMAIL admin_email = request.app.state.config.ADMIN_EMAIL
admin_name = None admin_name = None
print(admin_email, admin_name) log.info(f"Admin details - Email: {admin_email}, Name: {admin_name}")
if admin_email: if admin_email:
admin = Users.get_user_by_email(admin_email) admin = Users.get_user_by_email(admin_email)
+12
View File
@@ -75,6 +75,7 @@ class CodeInterpreterConfigForm(BaseModel):
CODE_EXECUTION_JUPYTER_AUTH: Optional[str] CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str] CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str] CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str]
CODE_EXECUTION_JUPYTER_TIMEOUT: Optional[int]
ENABLE_CODE_INTERPRETER: bool ENABLE_CODE_INTERPRETER: bool
CODE_INTERPRETER_ENGINE: str CODE_INTERPRETER_ENGINE: str
CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str] CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str]
@@ -82,6 +83,7 @@ class CodeInterpreterConfigForm(BaseModel):
CODE_INTERPRETER_JUPYTER_AUTH: Optional[str] CODE_INTERPRETER_JUPYTER_AUTH: Optional[str]
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str] CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str]
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str]
CODE_INTERPRETER_JUPYTER_TIMEOUT: Optional[int]
@router.get("/code_execution", response_model=CodeInterpreterConfigForm) @router.get("/code_execution", response_model=CodeInterpreterConfigForm)
@@ -92,6 +94,7 @@ async def get_code_execution_config(request: Request, user=Depends(get_admin_use
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
@@ -99,6 +102,7 @@ async def get_code_execution_config(request: Request, user=Depends(get_admin_use
"CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
"CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
} }
@@ -120,6 +124,9 @@ async def set_code_execution_config(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
) )
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = (
form_data.CODE_EXECUTION_JUPYTER_TIMEOUT
)
request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
@@ -141,6 +148,9 @@ async def set_code_execution_config(
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ( request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
) )
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = (
form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT
)
return { return {
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
@@ -148,6 +158,7 @@ async def set_code_execution_config(
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
@@ -155,6 +166,7 @@ async def set_code_execution_config(
"CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
"CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
} }
+34 -11
View File
@@ -16,6 +16,7 @@ from open_webui.models.files import (
Files, Files,
) )
from open_webui.routers.retrieval import ProcessFileForm, process_file from open_webui.routers.retrieval import ProcessFileForm, process_file
from open_webui.routers.audio import transcribe
from open_webui.storage.provider import Storage from open_webui.storage.provider import Storage
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from pydantic import BaseModel from pydantic import BaseModel
@@ -67,7 +68,22 @@ def upload_file(
) )
try: try:
process_file(request, ProcessFileForm(file_id=id), user=user) if file.content_type in [
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
]:
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
else:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id) file_item = Files.get_file_by_id(id=id)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
@@ -225,17 +241,24 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
filename = file.meta.get("name", file.filename) filename = file.meta.get("name", file.filename)
encoded_filename = quote(filename) # RFC5987 encoding encoded_filename = quote(filename) # RFC5987 encoding
content_type = file.meta.get("content_type")
filename = file.meta.get("name", file.filename)
encoded_filename = quote(filename)
headers = {} 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) if content_type == "application/pdf" or filename.lower().endswith(
".pdf"
):
headers["Content-Disposition"] = (
f"inline; filename*=UTF-8''{encoded_filename}"
)
content_type = "application/pdf"
elif content_type != "text/plain":
headers["Content-Disposition"] = (
f"attachment; filename*=UTF-8''{encoded_filename}"
)
return FileResponse(file_path, headers=headers, media_type=content_type)
else: else:
raise HTTPException( raise HTTPException(
@@ -266,7 +289,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
# Check if the file already exists in the cache # Check if the file already exists in the cache
if file_path.is_file(): if file_path.is_file():
print(f"file_path: {file_path}") log.info(f"file_path: {file_path}")
return FileResponse(file_path) return FileResponse(file_path)
else: else:
raise HTTPException( raise HTTPException(
+10 -4
View File
@@ -1,4 +1,5 @@
import os import os
import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -13,6 +14,11 @@ from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter() router = APIRouter()
@@ -79,7 +85,7 @@ async def create_new_function(
detail=ERROR_MESSAGES.DEFAULT("Error creating function"), detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to create a new function: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
@@ -183,7 +189,7 @@ async def update_function_by_id(
FUNCTIONS[id] = function_module FUNCTIONS[id] = function_module
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type} updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
print(updated) log.debug(updated)
function = Functions.update_function_by_id(id, updated) function = Functions.update_function_by_id(id, updated)
@@ -299,7 +305,7 @@ async def update_function_valves_by_id(
Functions.update_function_valves_by_id(id, valves.model_dump()) Functions.update_function_valves_by_id(id, valves.model_dump())
return valves.model_dump() return valves.model_dump()
except Exception as e: except Exception as e:
print(e) log.exception(f"Error updating function values by id {id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
@@ -388,7 +394,7 @@ async def update_function_user_valves_by_id(
) )
return user_valves.model_dump() return user_valves.model_dump()
except Exception as e: except Exception as e:
print(e) log.exception(f"Error updating function user valves by id {id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
+11 -5
View File
@@ -1,7 +1,7 @@
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import logging
from open_webui.models.users import Users from open_webui.models.users import Users
from open_webui.models.groups import ( from open_webui.models.groups import (
@@ -14,7 +14,13 @@ from open_webui.models.groups import (
from open_webui.config import CACHE_DIR from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter() router = APIRouter()
@@ -37,7 +43,7 @@ async def get_groups(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[GroupResponse]) @router.post("/create", response_model=Optional[GroupResponse])
async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)): async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)):
try: try:
group = Groups.insert_new_group(user.id, form_data) group = Groups.insert_new_group(user.id, form_data)
if group: if group:
@@ -48,7 +54,7 @@ async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)
detail=ERROR_MESSAGES.DEFAULT("Error creating group"), detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Error creating a new group: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
@@ -94,7 +100,7 @@ async def update_group_by_id(
detail=ERROR_MESSAGES.DEFAULT("Error updating group"), detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Error updating group {id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
@@ -118,7 +124,7 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user)):
detail=ERROR_MESSAGES.DEFAULT("Error deleting group"), detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Error deleting group {id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
+76 -2
View File
@@ -55,6 +55,10 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
"COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW, "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
"COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES, "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
}, },
"gemini": {
"GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
"GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
},
} }
@@ -78,6 +82,11 @@ class ComfyUIConfigForm(BaseModel):
COMFYUI_WORKFLOW_NODES: list[dict] COMFYUI_WORKFLOW_NODES: list[dict]
class GeminiConfigForm(BaseModel):
GEMINI_API_BASE_URL: str
GEMINI_API_KEY: str
class ConfigForm(BaseModel): class ConfigForm(BaseModel):
enabled: bool enabled: bool
engine: str engine: str
@@ -85,6 +94,7 @@ class ConfigForm(BaseModel):
openai: OpenAIConfigForm openai: OpenAIConfigForm
automatic1111: Automatic1111ConfigForm automatic1111: Automatic1111ConfigForm
comfyui: ComfyUIConfigForm comfyui: ComfyUIConfigForm
gemini: GeminiConfigForm
@router.post("/config/update") @router.post("/config/update")
@@ -103,6 +113,11 @@ async def update_config(
) )
request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
request.app.state.config.IMAGES_GEMINI_API_BASE_URL = (
form_data.gemini.GEMINI_API_BASE_URL
)
request.app.state.config.IMAGES_GEMINI_API_KEY = form_data.gemini.GEMINI_API_KEY
request.app.state.config.AUTOMATIC1111_BASE_URL = ( request.app.state.config.AUTOMATIC1111_BASE_URL = (
form_data.automatic1111.AUTOMATIC1111_BASE_URL form_data.automatic1111.AUTOMATIC1111_BASE_URL
) )
@@ -129,6 +144,8 @@ async def update_config(
request.app.state.config.COMFYUI_BASE_URL = ( request.app.state.config.COMFYUI_BASE_URL = (
form_data.comfyui.COMFYUI_BASE_URL.strip("/") form_data.comfyui.COMFYUI_BASE_URL.strip("/")
) )
request.app.state.config.COMFYUI_API_KEY = form_data.comfyui.COMFYUI_API_KEY
request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
request.app.state.config.COMFYUI_WORKFLOW_NODES = ( request.app.state.config.COMFYUI_WORKFLOW_NODES = (
form_data.comfyui.COMFYUI_WORKFLOW_NODES form_data.comfyui.COMFYUI_WORKFLOW_NODES
@@ -155,6 +172,10 @@ async def update_config(
"COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW, "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
"COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES, "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
}, },
"gemini": {
"GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
"GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
},
} }
@@ -184,9 +205,17 @@ async def verify_url(request: Request, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_IMAGE_GENERATION = False request.app.state.config.ENABLE_IMAGE_GENERATION = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
headers = None
if request.app.state.config.COMFYUI_API_KEY:
headers = {
"Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}"
}
try: try:
r = requests.get( r = requests.get(
url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info" url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info",
headers=headers,
) )
r.raise_for_status() r.raise_for_status()
return True return True
@@ -224,6 +253,12 @@ def get_image_model(request):
if request.app.state.config.IMAGE_GENERATION_MODEL if request.app.state.config.IMAGE_GENERATION_MODEL
else "dall-e-2" else "dall-e-2"
) )
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
return (
request.app.state.config.IMAGE_GENERATION_MODEL
if request.app.state.config.IMAGE_GENERATION_MODEL
else "imagen-3.0-generate-002"
)
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
return ( return (
request.app.state.config.IMAGE_GENERATION_MODEL request.app.state.config.IMAGE_GENERATION_MODEL
@@ -299,6 +334,10 @@ def get_models(request: Request, user=Depends(get_verified_user)):
{"id": "dall-e-2", "name": "DALL·E 2"}, {"id": "dall-e-2", "name": "DALL·E 2"},
{"id": "dall-e-3", "name": "DALL·E 3"}, {"id": "dall-e-3", "name": "DALL·E 3"},
] ]
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
return [
{"id": "imagen-3-0-generate-002", "name": "imagen-3.0 generate-002"},
]
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
# TODO - get models from comfyui # TODO - get models from comfyui
headers = { headers = {
@@ -322,7 +361,7 @@ def get_models(request: Request, user=Depends(get_verified_user)):
if model_node_id: if model_node_id:
model_list_key = None model_list_key = None
print(workflow[model_node_id]["class_type"]) log.info(workflow[model_node_id]["class_type"])
for key in info[workflow[model_node_id]["class_type"]]["input"][ for key in info[workflow[model_node_id]["class_type"]]["input"][
"required" "required"
]: ]:
@@ -483,6 +522,41 @@ async def image_generations(
images.append({"url": url}) images.append({"url": url})
return images return images
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
headers = {}
headers["Content-Type"] = "application/json"
headers["x-goog-api-key"] = request.app.state.config.IMAGES_GEMINI_API_KEY
model = get_image_model(request)
data = {
"instances": {"prompt": form_data.prompt},
"parameters": {
"sampleCount": form_data.n,
"outputOptions": {"mimeType": "image/png"},
},
}
# Use asyncio.to_thread for the requests.post call
r = await asyncio.to_thread(
requests.post,
url=f"{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}:predict",
json=data,
headers=headers,
)
r.raise_for_status()
res = r.json()
images = []
for image in res["predictions"]:
image_data, content_type = load_b64_image_data(
image["bytesBase64Encoded"]
)
url = upload_image(request, data, image_data, content_type, user)
images.append({"url": url})
return images
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
data = { data = {
"prompt": form_data.prompt, "prompt": form_data.prompt,
+1 -1
View File
@@ -614,7 +614,7 @@ def add_files_to_knowledge_batch(
) )
# Get files content # Get files content
print(f"files/batch/add - {len(form_data)} files") log.info(f"files/batch/add - {len(form_data)} files")
files: List[FileModel] = [] files: List[FileModel] = []
for form in form_data: for form in form_data:
file = Files.get_file_by_id(form.file_id) file = Files.get_file_by_id(form.file_id)
+145 -18
View File
@@ -14,6 +14,11 @@ from urllib.parse import urlparse
import aiohttp import aiohttp
from aiocache import cached from aiocache import cached
import requests import requests
from open_webui.models.users import UserModel
from open_webui.env import (
ENABLE_FORWARD_USER_INFO_HEADERS,
)
from fastapi import ( from fastapi import (
Depends, Depends,
@@ -26,7 +31,7 @@ from fastapi import (
) )
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict, validator
from starlette.background import BackgroundTask from starlette.background import BackgroundTask
@@ -66,12 +71,26 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
########################################## ##########################################
async def send_get_request(url, key=None): async def send_get_request(url, key=None, user: UserModel = None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST) timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
try: try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get( async with session.get(
url, headers={**({"Authorization": f"Bearer {key}"} if key else {})} url,
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
},
) as response: ) as response:
return await response.json() return await response.json()
except Exception as e: except Exception as e:
@@ -96,6 +115,7 @@ async def send_post_request(
stream: bool = True, stream: bool = True,
key: Optional[str] = None, key: Optional[str] = None,
content_type: Optional[str] = None, content_type: Optional[str] = None,
user: UserModel = None,
): ):
r = None r = None
@@ -110,6 +130,16 @@ async def send_post_request(
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}), **({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
}, },
) )
r.raise_for_status() r.raise_for_status()
@@ -191,7 +221,19 @@ async def verify_connection(
try: try:
async with session.get( async with session.get(
f"{url}/api/version", f"{url}/api/version",
headers={**({"Authorization": f"Bearer {key}"} if key else {})}, headers={
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
},
) as r: ) as r:
if r.status != 200: if r.status != 200:
detail = f"HTTP Error: {r.status}" detail = f"HTTP Error: {r.status}"
@@ -254,7 +296,7 @@ async def update_config(
@cached(ttl=3) @cached(ttl=3)
async def get_all_models(request: Request): async def get_all_models(request: Request, user: UserModel = None):
log.info("get_all_models()") log.info("get_all_models()")
if request.app.state.config.ENABLE_OLLAMA_API: if request.app.state.config.ENABLE_OLLAMA_API:
request_tasks = [] request_tasks = []
@@ -262,7 +304,7 @@ async def get_all_models(request: Request):
if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and ( if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and (
url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support
): ):
request_tasks.append(send_get_request(f"{url}/api/tags")) request_tasks.append(send_get_request(f"{url}/api/tags", user=user))
else: else:
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx), str(idx),
@@ -275,7 +317,9 @@ async def get_all_models(request: Request):
key = api_config.get("key", None) key = api_config.get("key", None)
if enable: if enable:
request_tasks.append(send_get_request(f"{url}/api/tags", key)) request_tasks.append(
send_get_request(f"{url}/api/tags", key, user=user)
)
else: else:
request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
@@ -360,7 +404,7 @@ async def get_ollama_tags(
models = [] models = []
if url_idx is None: if url_idx is None:
models = await get_all_models(request) models = await get_all_models(request, user=user)
else: else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
@@ -370,7 +414,19 @@ async def get_ollama_tags(
r = requests.request( r = requests.request(
method="GET", method="GET",
url=f"{url}/api/tags", url=f"{url}/api/tags",
headers={**({"Authorization": f"Bearer {key}"} if key else {})}, headers={
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
},
) )
r.raise_for_status() r.raise_for_status()
@@ -477,6 +533,7 @@ async def get_ollama_loaded_models(request: Request, user=Depends(get_verified_u
url, {} url, {}
), # Legacy support ), # Legacy support
).get("key", None), ).get("key", None),
user=user,
) )
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS) for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
] ]
@@ -509,6 +566,7 @@ async def pull_model(
url=f"{url}/api/pull", url=f"{url}/api/pull",
payload=json.dumps(payload), payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
) )
@@ -527,7 +585,7 @@ async def push_model(
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
if url_idx is None: if url_idx is None:
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
if form_data.name in models: if form_data.name in models:
@@ -545,6 +603,7 @@ async def push_model(
url=f"{url}/api/push", url=f"{url}/api/push",
payload=form_data.model_dump_json(exclude_none=True).encode(), payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
) )
@@ -571,6 +630,7 @@ async def create_model(
url=f"{url}/api/create", url=f"{url}/api/create",
payload=form_data.model_dump_json(exclude_none=True).encode(), payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
) )
@@ -588,7 +648,7 @@ async def copy_model(
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
if url_idx is None: if url_idx is None:
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
if form_data.source in models: if form_data.source in models:
@@ -609,6 +669,16 @@ async def copy_model(
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}), **({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
}, },
data=form_data.model_dump_json(exclude_none=True).encode(), data=form_data.model_dump_json(exclude_none=True).encode(),
) )
@@ -643,7 +713,7 @@ async def delete_model(
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
if url_idx is None: if url_idx is None:
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
if form_data.name in models: if form_data.name in models:
@@ -665,6 +735,16 @@ async def delete_model(
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}), **({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
}, },
) )
r.raise_for_status() r.raise_for_status()
@@ -693,7 +773,7 @@ async def delete_model(
async def show_model_info( async def show_model_info(
request: Request, form_data: ModelNameForm, user=Depends(get_verified_user) request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)
): ):
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
if form_data.name not in models: if form_data.name not in models:
@@ -714,6 +794,16 @@ async def show_model_info(
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}), **({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
}, },
data=form_data.model_dump_json(exclude_none=True).encode(), data=form_data.model_dump_json(exclude_none=True).encode(),
) )
@@ -757,7 +847,7 @@ async def embed(
log.info(f"generate_ollama_batch_embeddings {form_data}") log.info(f"generate_ollama_batch_embeddings {form_data}")
if url_idx is None: if url_idx is None:
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
model = form_data.model model = form_data.model
@@ -783,6 +873,16 @@ async def embed(
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}), **({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
}, },
data=form_data.model_dump_json(exclude_none=True).encode(), data=form_data.model_dump_json(exclude_none=True).encode(),
) )
@@ -826,7 +926,7 @@ async def embeddings(
log.info(f"generate_ollama_embeddings {form_data}") log.info(f"generate_ollama_embeddings {form_data}")
if url_idx is None: if url_idx is None:
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
model = form_data.model model = form_data.model
@@ -852,6 +952,16 @@ async def embeddings(
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}), **({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
}, },
data=form_data.model_dump_json(exclude_none=True).encode(), data=form_data.model_dump_json(exclude_none=True).encode(),
) )
@@ -901,7 +1011,7 @@ async def generate_completion(
user=Depends(get_verified_user), user=Depends(get_verified_user),
): ):
if url_idx is None: if url_idx is None:
await get_all_models(request) await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS models = request.app.state.OLLAMA_MODELS
model = form_data.model model = form_data.model
@@ -931,15 +1041,29 @@ async def generate_completion(
url=f"{url}/api/generate", url=f"{url}/api/generate",
payload=form_data.model_dump_json(exclude_none=True).encode(), payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
) )
class ChatMessage(BaseModel): class ChatMessage(BaseModel):
role: str role: str
content: str content: Optional[str] = None
tool_calls: Optional[list[dict]] = None tool_calls: Optional[list[dict]] = None
images: Optional[list[str]] = None images: Optional[list[str]] = None
@validator("content", pre=True)
@classmethod
def check_at_least_one_field(cls, field_value, values, **kwargs):
# Raise an error if both 'content' and 'tool_calls' are None
if field_value is None and (
"tool_calls" not in values or values["tool_calls"] is None
):
raise ValueError(
"At least one of 'content' or 'tool_calls' must be provided"
)
return field_value
class GenerateChatCompletionForm(BaseModel): class GenerateChatCompletionForm(BaseModel):
model: str model: str
@@ -1047,6 +1171,7 @@ async def generate_chat_completion(
stream=form_data.stream, stream=form_data.stream,
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
content_type="application/x-ndjson", content_type="application/x-ndjson",
user=user,
) )
@@ -1149,6 +1274,7 @@ async def generate_openai_completion(
payload=json.dumps(payload), payload=json.dumps(payload),
stream=payload.get("stream", False), stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
) )
@@ -1227,6 +1353,7 @@ async def generate_openai_chat_completion(
payload=json.dumps(payload), payload=json.dumps(payload),
stream=payload.get("stream", False), stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
) )
@@ -1240,7 +1367,7 @@ async def get_openai_models(
models = [] models = []
if url_idx is None: if url_idx is None:
model_list = await get_all_models(request) model_list = await get_all_models(request, user=user)
models = [ models = [
{ {
"id": model["model"], "id": model["model"],
+44 -11
View File
@@ -26,6 +26,7 @@ from open_webui.env import (
ENABLE_FORWARD_USER_INFO_HEADERS, ENABLE_FORWARD_USER_INFO_HEADERS,
BYPASS_MODEL_ACCESS_CONTROL, BYPASS_MODEL_ACCESS_CONTROL,
) )
from open_webui.models.users import UserModel
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ENV, SRC_LOG_LEVELS from open_webui.env import ENV, SRC_LOG_LEVELS
@@ -51,12 +52,25 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
########################################## ##########################################
async def send_get_request(url, key=None): async def send_get_request(url, key=None, user: UserModel = None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST) timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
try: try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get( async with session.get(
url, headers={**({"Authorization": f"Bearer {key}"} if key else {})} url,
headers={
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"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 and user
else {}
),
},
) as response: ) as response:
return await response.json() return await response.json()
except Exception as e: except Exception as e:
@@ -84,9 +98,15 @@ def openai_o1_o3_handler(payload):
payload["max_completion_tokens"] = payload["max_tokens"] payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"] del payload["max_tokens"]
# Fix: O1 does not support the "system" parameter, Modify "system" to "user" # Fix: o1 and o3 do not support the "system" role directly.
# For older models like "o1-mini" or "o1-preview", use role "user".
# For newer o1/o3 models, replace "system" with "developer".
if payload["messages"][0]["role"] == "system": if payload["messages"][0]["role"] == "system":
payload["messages"][0]["role"] = "user" model_lower = payload["model"].lower()
if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"):
payload["messages"][0]["role"] = "user"
else:
payload["messages"][0]["role"] = "developer"
return payload return payload
@@ -247,7 +267,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
async def get_all_models_responses(request: Request) -> list: async def get_all_models_responses(request: Request, user: UserModel) -> list:
if not request.app.state.config.ENABLE_OPENAI_API: if not request.app.state.config.ENABLE_OPENAI_API:
return [] return []
@@ -271,7 +291,9 @@ async def get_all_models_responses(request: Request) -> list:
): ):
request_tasks.append( request_tasks.append(
send_get_request( send_get_request(
f"{url}/models", request.app.state.config.OPENAI_API_KEYS[idx] f"{url}/models",
request.app.state.config.OPENAI_API_KEYS[idx],
user=user,
) )
) )
else: else:
@@ -291,6 +313,7 @@ async def get_all_models_responses(request: Request) -> list:
send_get_request( send_get_request(
f"{url}/models", f"{url}/models",
request.app.state.config.OPENAI_API_KEYS[idx], request.app.state.config.OPENAI_API_KEYS[idx],
user=user,
) )
) )
else: else:
@@ -352,13 +375,13 @@ async def get_filtered_models(models, user):
@cached(ttl=3) @cached(ttl=3)
async def get_all_models(request: Request) -> dict[str, list]: async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
log.info("get_all_models()") log.info("get_all_models()")
if not request.app.state.config.ENABLE_OPENAI_API: if not request.app.state.config.ENABLE_OPENAI_API:
return {"data": []} return {"data": []}
responses = await get_all_models_responses(request) responses = await get_all_models_responses(request, user=user)
def extract_data(response): def extract_data(response):
if response and "data" in response: if response and "data" in response:
@@ -418,7 +441,7 @@ async def get_models(
} }
if url_idx is None: if url_idx is None:
models = await get_all_models(request) models = await get_all_models(request, user=user)
else: else:
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx] url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = request.app.state.config.OPENAI_API_KEYS[url_idx] key = request.app.state.config.OPENAI_API_KEYS[url_idx]
@@ -515,6 +538,16 @@ async def verify_connection(
headers={ headers={
"Authorization": f"Bearer {key}", "Authorization": f"Bearer {key}",
"Content-Type": "application/json", "Content-Type": "application/json",
**(
{
"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: ) as r:
if r.status != 200: if r.status != 200:
@@ -587,7 +620,7 @@ async def generate_chat_completion(
detail="Model not found", detail="Model not found",
) )
await get_all_models(request) await get_all_models(request, user=user)
model = request.app.state.OPENAI_MODELS.get(model_id) model = request.app.state.OPENAI_MODELS.get(model_id)
if model: if model:
idx = model["urlIdx"] idx = model["urlIdx"]
@@ -777,7 +810,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
if r is not None: if r is not None:
try: try:
res = await r.json() res = await r.json()
print(res) log.error(res)
if "error" in res: if "error" in res:
detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}" detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except Exception: except Exception:
+11 -11
View File
@@ -101,7 +101,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
if "detail" in res: if "detail" in res:
raise Exception(response.status, res["detail"]) raise Exception(response.status, res["detail"])
except Exception as e: except Exception as e:
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
return payload return payload
@@ -153,7 +153,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
except Exception: except Exception:
pass pass
except Exception as e: except Exception as e:
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
return payload return payload
@@ -169,7 +169,7 @@ router = APIRouter()
@router.get("/list") @router.get("/list")
async def get_pipelines_list(request: Request, user=Depends(get_admin_user)): async def get_pipelines_list(request: Request, user=Depends(get_admin_user)):
responses = await get_all_models_responses(request) responses = await get_all_models_responses(request, user)
log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}") log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}")
urlIdxs = [ urlIdxs = [
@@ -196,7 +196,7 @@ async def upload_pipeline(
file: UploadFile = File(...), file: UploadFile = File(...),
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
print("upload_pipeline", urlIdx, file.filename) log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
# Check if the uploaded file is a python file # Check if the uploaded file is a python file
if not (file.filename and file.filename.endswith(".py")): if not (file.filename and file.filename.endswith(".py")):
raise HTTPException( raise HTTPException(
@@ -231,7 +231,7 @@ async def upload_pipeline(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
status_code = status.HTTP_404_NOT_FOUND status_code = status.HTTP_404_NOT_FOUND
@@ -282,7 +282,7 @@ async def add_pipeline(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
if r is not None: if r is not None:
@@ -327,7 +327,7 @@ async def delete_pipeline(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
if r is not None: if r is not None:
@@ -361,7 +361,7 @@ async def get_pipelines(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
if r is not None: if r is not None:
@@ -400,7 +400,7 @@ async def get_pipeline_valves(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
if r is not None: if r is not None:
@@ -440,7 +440,7 @@ async def get_pipeline_valves_spec(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
if r is not None: if r is not None:
@@ -482,7 +482,7 @@ async def update_pipeline_valves(
return {**data} return {**data}
except Exception as e: except Exception as e:
# Handle connection error here # Handle connection error here
print(f"Connection error: {e}") log.exception(f"Connection error: {e}")
detail = None detail = None
+107 -39
View File
@@ -351,10 +351,17 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
return { return {
"status": True, "status": True,
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES, "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
"content_extraction": { "content_extraction": {
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": request.app.state.config.TIKA_SERVER_URL, "tika_server_url": request.app.state.config.TIKA_SERVER_URL,
"document_intelligence_config": {
"endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
},
}, },
"chunk": { "chunk": {
"text_splitter": request.app.state.config.TEXT_SPLITTER, "text_splitter": request.app.state.config.TEXT_SPLITTER,
@@ -372,10 +379,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
}, },
"web": { "web": {
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT, "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
"search": { "search": {
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"onedrive": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE, "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL, "searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
"google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY, "google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
@@ -398,6 +406,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
"exa_api_key": request.app.state.config.EXA_API_KEY, "exa_api_key": request.app.state.config.EXA_API_KEY,
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
"domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
}, },
@@ -410,9 +419,15 @@ class FileConfig(BaseModel):
max_count: Optional[int] = None max_count: Optional[int] = None
class DocumentIntelligenceConfigForm(BaseModel):
endpoint: str
key: str
class ContentExtractionConfig(BaseModel): class ContentExtractionConfig(BaseModel):
engine: str = "" engine: str = ""
tika_server_url: Optional[str] = None tika_server_url: Optional[str] = None
document_intelligence_config: Optional[DocumentIntelligenceConfigForm] = None
class ChunkParamUpdateForm(BaseModel): class ChunkParamUpdateForm(BaseModel):
@@ -459,12 +474,15 @@ class WebSearchConfig(BaseModel):
class WebConfig(BaseModel): class WebConfig(BaseModel):
search: WebSearchConfig search: WebSearchConfig
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
class ConfigUpdateForm(BaseModel): class ConfigUpdateForm(BaseModel):
RAG_FULL_CONTEXT: Optional[bool] = None
BYPASS_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
pdf_extract_images: Optional[bool] = None pdf_extract_images: Optional[bool] = None
enable_google_drive_integration: Optional[bool] = None enable_google_drive_integration: Optional[bool] = None
enable_onedrive_integration: Optional[bool] = None
file: Optional[FileConfig] = None file: Optional[FileConfig] = None
content_extraction: Optional[ContentExtractionConfig] = None content_extraction: Optional[ContentExtractionConfig] = None
chunk: Optional[ChunkParamUpdateForm] = None chunk: Optional[ChunkParamUpdateForm] = None
@@ -482,24 +500,51 @@ async def update_rag_config(
else request.app.state.config.PDF_EXTRACT_IMAGES else request.app.state.config.PDF_EXTRACT_IMAGES
) )
request.app.state.config.RAG_FULL_CONTEXT = (
form_data.RAG_FULL_CONTEXT
if form_data.RAG_FULL_CONTEXT is not None
else request.app.state.config.RAG_FULL_CONTEXT
)
request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = (
form_data.BYPASS_EMBEDDING_AND_RETRIEVAL
if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None
else request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
)
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ( request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
form_data.enable_google_drive_integration form_data.enable_google_drive_integration
if form_data.enable_google_drive_integration is not None if form_data.enable_google_drive_integration is not None
else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
) )
request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
form_data.enable_onedrive_integration
if form_data.enable_onedrive_integration is not None
else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
)
if form_data.file is not None: if form_data.file is not None:
request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
if form_data.content_extraction is not None: if form_data.content_extraction is not None:
log.info(f"Updating text settings: {form_data.content_extraction}") log.info(
f"Updating content extraction: {request.app.state.config.CONTENT_EXTRACTION_ENGINE} to {form_data.content_extraction.engine}"
)
request.app.state.config.CONTENT_EXTRACTION_ENGINE = ( request.app.state.config.CONTENT_EXTRACTION_ENGINE = (
form_data.content_extraction.engine form_data.content_extraction.engine
) )
request.app.state.config.TIKA_SERVER_URL = ( request.app.state.config.TIKA_SERVER_URL = (
form_data.content_extraction.tika_server_url form_data.content_extraction.tika_server_url
) )
if form_data.content_extraction.document_intelligence_config is not None:
request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
form_data.content_extraction.document_intelligence_config.endpoint
)
request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = (
form_data.content_extraction.document_intelligence_config.key
)
if form_data.chunk is not None: if form_data.chunk is not None:
request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter
@@ -520,8 +565,8 @@ async def update_rag_config(
request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = ( request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
) )
request.app.state.config.SEARXNG_QUERY_URL = ( request.app.state.config.SEARXNG_QUERY_URL = (
@@ -588,6 +633,8 @@ async def update_rag_config(
return { return {
"status": True, "status": True,
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES, "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
"file": { "file": {
"max_size": request.app.state.config.FILE_MAX_SIZE, "max_size": request.app.state.config.FILE_MAX_SIZE,
"max_count": request.app.state.config.FILE_MAX_COUNT, "max_count": request.app.state.config.FILE_MAX_COUNT,
@@ -595,6 +642,10 @@ async def update_rag_config(
"content_extraction": { "content_extraction": {
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": request.app.state.config.TIKA_SERVER_URL, "tika_server_url": request.app.state.config.TIKA_SERVER_URL,
"document_intelligence_config": {
"endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
},
}, },
"chunk": { "chunk": {
"text_splitter": request.app.state.config.TEXT_SPLITTER, "text_splitter": request.app.state.config.TEXT_SPLITTER,
@@ -608,7 +659,7 @@ async def update_rag_config(
}, },
"web": { "web": {
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT, "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
"search": { "search": {
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE, "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
@@ -871,7 +922,12 @@ def process_file(
# Update the content in the file # Update the content in the file
# Usage: /files/{file_id}/data/content/update # Usage: /files/{file_id}/data/content/update
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}") try:
# /files/{file_id}/data/content/update
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
except:
# Audio file upload pipeline
pass
docs = [ docs = [
Document( Document(
@@ -928,6 +984,8 @@ def process_file(
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE, engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL, TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
) )
docs = loader.load( docs = loader.load(
file.filename, file.meta.get("content_type"), file_path file.filename, file.meta.get("content_type"), file_path
@@ -970,36 +1028,45 @@ def process_file(
hash = calculate_sha256_string(text_content) hash = calculate_sha256_string(text_content)
Files.update_file_hash_by_id(file.id, hash) Files.update_file_hash_by_id(file.id, hash)
try: if not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
result = save_docs_to_vector_db( try:
request, result = save_docs_to_vector_db(
docs=docs, request,
collection_name=collection_name, docs=docs,
metadata={ collection_name=collection_name,
"file_id": file.id, metadata={
"name": file.filename, "file_id": file.id,
"hash": hash, "name": file.filename,
}, "hash": hash,
add=(True if form_data.collection_name else False),
user=user,
)
if result:
Files.update_file_metadata_by_id(
file.id,
{
"collection_name": collection_name,
}, },
add=(True if form_data.collection_name else False),
user=user,
) )
return { if result:
"status": True, Files.update_file_metadata_by_id(
"collection_name": collection_name, file.id,
"filename": file.filename, {
"content": text_content, "collection_name": collection_name,
} },
except Exception as e: )
raise e
return {
"status": True,
"collection_name": collection_name,
"filename": file.filename,
"content": text_content,
}
except Exception as e:
raise e
else:
return {
"status": True,
"collection_name": None,
"filename": file.filename,
"content": text_content,
}
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
if "No pandoc was found" in str(e): if "No pandoc was found" in str(e):
@@ -1359,9 +1426,11 @@ async def process_web_search(
) )
docs = await loader.aload() docs = await loader.aload()
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT: if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
return { return {
"status": True, "status": True,
"collection_name": None,
"filenames": urls,
"docs": [ "docs": [
{ {
"content": doc.page_content, "content": doc.page_content,
@@ -1369,7 +1438,6 @@ async def process_web_search(
} }
for doc in docs for doc in docs
], ],
"filenames": urls,
"loaded_count": len(docs), "loaded_count": len(docs),
} }
else: else:
@@ -1544,11 +1612,11 @@ def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
elif os.path.isdir(file_path): elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory shutil.rmtree(file_path) # Remove the directory
except Exception as e: except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}") log.exception(f"Failed to delete {file_path}. Reason: {e}")
else: else:
print(f"The directory {folder} does not exist") log.warning(f"The directory {folder} does not exist")
except Exception as e: except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}") log.exception(f"Failed to process the directory {folder}. Reason: {e}")
return True return True
+46
View File
@@ -20,6 +20,10 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.constants import TASKS from open_webui.constants import TASKS
from open_webui.routers.pipelines import process_pipeline_inlet_filter from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.filter import (
get_sorted_filter_ids,
process_filter_functions,
)
from open_webui.utils.task import get_task_model_id from open_webui.utils.task import get_task_model_id
from open_webui.config import ( from open_webui.config import (
@@ -221,6 +225,12 @@ async def generate_title(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
@@ -290,6 +300,12 @@ async def generate_chat_tags(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
@@ -356,6 +372,12 @@ async def generate_image_prompt(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
@@ -433,6 +455,12 @@ async def generate_queries(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
@@ -514,6 +542,12 @@ async def generate_autocompletion(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
@@ -584,6 +618,12 @@ async def generate_emoji(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
@@ -644,6 +684,12 @@ async def generate_moa_response(
}, },
} }
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try: try:
return await generate_chat_completion(request, form_data=payload, user=user) return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e: except Exception as e:
+9 -4
View File
@@ -1,3 +1,4 @@
import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -15,6 +16,10 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.tools import get_tools_specs from open_webui.utils.tools import get_tools_specs
from open_webui.utils.auth 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_access, has_permission from open_webui.utils.access_control import has_access, has_permission
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter() router = APIRouter()
@@ -111,7 +116,7 @@ async def create_new_tools(
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"), detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to load the tool by id {form_data.id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)), detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -193,7 +198,7 @@ async def update_tools_by_id(
"specs": specs, "specs": specs,
} }
print(updated) log.debug(updated)
tools = Tools.update_tool_by_id(id, updated) tools = Tools.update_tool_by_id(id, updated)
if tools: if tools:
@@ -343,7 +348,7 @@ async def update_tools_valves_by_id(
Tools.update_tool_valves_by_id(id, valves.model_dump()) Tools.update_tool_valves_by_id(id, valves.model_dump())
return valves.model_dump() return valves.model_dump()
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to update tool valves by id {id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)), detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -421,7 +426,7 @@ async def update_tools_user_valves_by_id(
) )
return user_valves.model_dump() return user_valves.model_dump()
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to update user valves by id {id}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)), detail=ERROR_MESSAGES.DEFAULT(str(e)),
+7 -1
View File
@@ -1,4 +1,5 @@
import black import black
import logging
import markdown import markdown
from open_webui.models.chats import ChatTitleMessagesForm from open_webui.models.chats import ChatTitleMessagesForm
@@ -13,8 +14,12 @@ from open_webui.utils.misc import get_gravatar_url
from open_webui.utils.pdf_generator import PDFGenerator from open_webui.utils.pdf_generator import PDFGenerator
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.code_interpreter import execute_code_jupyter from open_webui.utils.code_interpreter import execute_code_jupyter
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter() router = APIRouter()
@@ -56,6 +61,7 @@ async def execute_code(
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password" if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password"
else None else None
), ),
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
) )
return output return output
@@ -95,7 +101,7 @@ async def download_chat_as_pdf(
headers={"Content-Disposition": "attachment;filename=chat.pdf"}, headers={"Content-Disposition": "attachment;filename=chat.pdf"},
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Error generating PDF: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
+94 -3
View File
@@ -1,10 +1,12 @@
import os import os
import shutil import shutil
import json import json
import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import BinaryIO, Tuple from typing import BinaryIO, Tuple
import boto3 import boto3
from botocore.config import Config
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from open_webui.config import ( from open_webui.config import (
S3_ACCESS_KEY_ID, S3_ACCESS_KEY_ID,
@@ -13,14 +15,27 @@ from open_webui.config import (
S3_KEY_PREFIX, S3_KEY_PREFIX,
S3_REGION_NAME, S3_REGION_NAME,
S3_SECRET_ACCESS_KEY, S3_SECRET_ACCESS_KEY,
S3_USE_ACCELERATE_ENDPOINT,
S3_ADDRESSING_STYLE,
GCS_BUCKET_NAME, GCS_BUCKET_NAME,
GOOGLE_APPLICATION_CREDENTIALS_JSON, GOOGLE_APPLICATION_CREDENTIALS_JSON,
AZURE_STORAGE_ENDPOINT,
AZURE_STORAGE_CONTAINER_NAME,
AZURE_STORAGE_KEY,
STORAGE_PROVIDER, STORAGE_PROVIDER,
UPLOAD_DIR, UPLOAD_DIR,
) )
from google.cloud import storage from google.cloud import storage
from google.cloud.exceptions import GoogleCloudError, NotFound from google.cloud.exceptions import GoogleCloudError, NotFound
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceNotFoundError
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
class StorageProvider(ABC): class StorageProvider(ABC):
@@ -65,7 +80,7 @@ class LocalStorageProvider(StorageProvider):
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.remove(file_path) os.remove(file_path)
else: else:
print(f"File {file_path} not found in local storage.") log.warning(f"File {file_path} not found in local storage.")
@staticmethod @staticmethod
def delete_all_files() -> None: def delete_all_files() -> None:
@@ -79,9 +94,9 @@ class LocalStorageProvider(StorageProvider):
elif os.path.isdir(file_path): elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory shutil.rmtree(file_path) # Remove the directory
except Exception as e: except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}") log.exception(f"Failed to delete {file_path}. Reason: {e}")
else: else:
print(f"Directory {UPLOAD_DIR} not found in local storage.") log.warning(f"Directory {UPLOAD_DIR} not found in local storage.")
class S3StorageProvider(StorageProvider): class S3StorageProvider(StorageProvider):
@@ -92,6 +107,12 @@ class S3StorageProvider(StorageProvider):
endpoint_url=S3_ENDPOINT_URL, endpoint_url=S3_ENDPOINT_URL,
aws_access_key_id=S3_ACCESS_KEY_ID, aws_access_key_id=S3_ACCESS_KEY_ID,
aws_secret_access_key=S3_SECRET_ACCESS_KEY, aws_secret_access_key=S3_SECRET_ACCESS_KEY,
config=Config(
s3={
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
"addressing_style": S3_ADDRESSING_STYLE,
},
),
) )
self.bucket_name = S3_BUCKET_NAME self.bucket_name = S3_BUCKET_NAME
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else "" self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
@@ -221,6 +242,74 @@ class GCSStorageProvider(StorageProvider):
LocalStorageProvider.delete_all_files() LocalStorageProvider.delete_all_files()
class AzureStorageProvider(StorageProvider):
def __init__(self):
self.endpoint = AZURE_STORAGE_ENDPOINT
self.container_name = AZURE_STORAGE_CONTAINER_NAME
storage_key = AZURE_STORAGE_KEY
if storage_key:
# Configure using the Azure Storage Account Endpoint and Key
self.blob_service_client = BlobServiceClient(
account_url=self.endpoint, credential=storage_key
)
else:
# Configure using the Azure Storage Account Endpoint and DefaultAzureCredential
# If the key is not configured, then the DefaultAzureCredential will be used to support Managed Identity authentication
self.blob_service_client = BlobServiceClient(
account_url=self.endpoint, credential=DefaultAzureCredential()
)
self.container_client = self.blob_service_client.get_container_client(
self.container_name
)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
"""Handles uploading of the file to Azure Blob Storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
try:
blob_client = self.container_client.get_blob_client(filename)
blob_client.upload_blob(contents, overwrite=True)
return contents, f"{self.endpoint}/{self.container_name}/{filename}"
except Exception as e:
raise RuntimeError(f"Error uploading file to Azure Blob Storage: {e}")
def get_file(self, file_path: str) -> str:
"""Handles downloading of the file from Azure Blob Storage."""
try:
filename = file_path.split("/")[-1]
local_file_path = f"{UPLOAD_DIR}/{filename}"
blob_client = self.container_client.get_blob_client(filename)
with open(local_file_path, "wb") as download_file:
download_file.write(blob_client.download_blob().readall())
return local_file_path
except ResourceNotFoundError as e:
raise RuntimeError(f"Error downloading file from Azure Blob Storage: {e}")
def delete_file(self, file_path: str) -> None:
"""Handles deletion of the file from Azure Blob Storage."""
try:
filename = file_path.split("/")[-1]
blob_client = self.container_client.get_blob_client(filename)
blob_client.delete_blob()
except ResourceNotFoundError as e:
raise RuntimeError(f"Error deleting file from Azure Blob Storage: {e}")
# Always delete from local storage
LocalStorageProvider.delete_file(file_path)
def delete_all_files(self) -> None:
"""Handles deletion of all files from Azure Blob Storage."""
try:
blobs = self.container_client.list_blobs()
for blob in blobs:
self.container_client.delete_blob(blob.name)
except Exception as e:
raise RuntimeError(f"Error deleting all files from Azure Blob Storage: {e}")
# Always delete from local storage
LocalStorageProvider.delete_all_files()
def get_storage_provider(storage_provider: str): def get_storage_provider(storage_provider: str):
if storage_provider == "local": if storage_provider == "local":
Storage = LocalStorageProvider() Storage = LocalStorageProvider()
@@ -228,6 +317,8 @@ def get_storage_provider(storage_provider: str):
Storage = S3StorageProvider() Storage = S3StorageProvider()
elif storage_provider == "gcs": elif storage_provider == "gcs":
Storage = GCSStorageProvider() Storage = GCSStorageProvider()
elif storage_provider == "azure":
Storage = AzureStorageProvider()
else: else:
raise RuntimeError(f"Unsupported storage provider: {storage_provider}") raise RuntimeError(f"Unsupported storage provider: {storage_provider}")
return Storage return Storage
@@ -7,6 +7,8 @@ from moto import mock_aws
from open_webui.storage import provider from open_webui.storage import provider
from gcp_storage_emulator.server import create_server from gcp_storage_emulator.server import create_server
from google.cloud import storage from google.cloud import storage
from azure.storage.blob import BlobServiceClient, ContainerClient, BlobClient
from unittest.mock import MagicMock
def mock_upload_dir(monkeypatch, tmp_path): def mock_upload_dir(monkeypatch, tmp_path):
@@ -22,6 +24,7 @@ def test_imports():
provider.LocalStorageProvider provider.LocalStorageProvider
provider.S3StorageProvider provider.S3StorageProvider
provider.GCSStorageProvider provider.GCSStorageProvider
provider.AzureStorageProvider
provider.Storage provider.Storage
@@ -32,6 +35,8 @@ def test_get_storage_provider():
assert isinstance(Storage, provider.S3StorageProvider) assert isinstance(Storage, provider.S3StorageProvider)
Storage = provider.get_storage_provider("gcs") Storage = provider.get_storage_provider("gcs")
assert isinstance(Storage, provider.GCSStorageProvider) assert isinstance(Storage, provider.GCSStorageProvider)
Storage = provider.get_storage_provider("azure")
assert isinstance(Storage, provider.AzureStorageProvider)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
provider.get_storage_provider("invalid") provider.get_storage_provider("invalid")
@@ -48,6 +53,7 @@ def test_class_instantiation():
provider.LocalStorageProvider() provider.LocalStorageProvider()
provider.S3StorageProvider() provider.S3StorageProvider()
provider.GCSStorageProvider() provider.GCSStorageProvider()
provider.AzureStorageProvider()
class TestLocalStorageProvider: class TestLocalStorageProvider:
@@ -272,3 +278,147 @@ class TestGCSStorageProvider:
assert not (upload_dir / self.filename_extra).exists() assert not (upload_dir / self.filename_extra).exists()
assert self.Storage.bucket.get_blob(self.filename) == None assert self.Storage.bucket.get_blob(self.filename) == None
assert self.Storage.bucket.get_blob(self.filename_extra) == None assert self.Storage.bucket.get_blob(self.filename_extra) == None
class TestAzureStorageProvider:
def __init__(self):
super().__init__()
@pytest.fixture(scope="class")
def setup_storage(self, monkeypatch):
# Create mock Blob Service Client and related clients
mock_blob_service_client = MagicMock()
mock_container_client = MagicMock()
mock_blob_client = MagicMock()
# Set up return values for the mock
mock_blob_service_client.get_container_client.return_value = (
mock_container_client
)
mock_container_client.get_blob_client.return_value = mock_blob_client
# Monkeypatch the Azure classes to return our mocks
monkeypatch.setattr(
azure.storage.blob,
"BlobServiceClient",
lambda *args, **kwargs: mock_blob_service_client,
)
monkeypatch.setattr(
azure.storage.blob,
"ContainerClient",
lambda *args, **kwargs: mock_container_client,
)
monkeypatch.setattr(
azure.storage.blob, "BlobClient", lambda *args, **kwargs: mock_blob_client
)
self.Storage = provider.AzureStorageProvider()
self.Storage.endpoint = "https://myaccount.blob.core.windows.net"
self.Storage.container_name = "my-container"
self.file_content = b"test content"
self.filename = "test.txt"
self.filename_extra = "test_extra.txt"
self.file_bytesio_empty = io.BytesIO()
# Apply mocks to the Storage instance
self.Storage.blob_service_client = mock_blob_service_client
self.Storage.container_client = mock_container_client
def test_upload_file(self, monkeypatch, tmp_path):
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
# Simulate an error when container does not exist
self.Storage.container_client.get_blob_client.side_effect = Exception(
"Container does not exist"
)
with pytest.raises(Exception):
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
# Reset side effect and create container
self.Storage.container_client.get_blob_client.side_effect = None
self.Storage.create_container()
contents, azure_file_path = self.Storage.upload_file(
io.BytesIO(self.file_content), self.filename
)
# Assertions
self.Storage.container_client.get_blob_client.assert_called_with(self.filename)
self.Storage.container_client.get_blob_client().upload_blob.assert_called_once_with(
self.file_content, overwrite=True
)
assert contents == self.file_content
assert (
azure_file_path
== f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
)
assert (upload_dir / self.filename).exists()
assert (upload_dir / self.filename).read_bytes() == self.file_content
with pytest.raises(ValueError):
self.Storage.upload_file(self.file_bytesio_empty, self.filename)
def test_get_file(self, monkeypatch, tmp_path):
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
self.Storage.create_container()
# Mock upload behavior
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
# Mock blob download behavior
self.Storage.container_client.get_blob_client().download_blob().readall.return_value = (
self.file_content
)
file_url = f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
file_path = self.Storage.get_file(file_url)
assert file_path == str(upload_dir / self.filename)
assert (upload_dir / self.filename).exists()
assert (upload_dir / self.filename).read_bytes() == self.file_content
def test_delete_file(self, monkeypatch, tmp_path):
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
self.Storage.create_container()
# Mock file upload
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
# Mock deletion
self.Storage.container_client.get_blob_client().delete_blob.return_value = None
file_url = f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
self.Storage.delete_file(file_url)
self.Storage.container_client.get_blob_client().delete_blob.assert_called_once()
assert not (upload_dir / self.filename).exists()
def test_delete_all_files(self, monkeypatch, tmp_path):
upload_dir = mock_upload_dir(monkeypatch, tmp_path)
self.Storage.create_container()
# Mock file uploads
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra)
# Mock listing and deletion behavior
self.Storage.container_client.list_blobs.return_value = [
{"name": self.filename},
{"name": self.filename_extra},
]
self.Storage.container_client.get_blob_client().delete_blob.return_value = None
self.Storage.delete_all_files()
self.Storage.container_client.list_blobs.assert_called_once()
self.Storage.container_client.get_blob_client().delete_blob.assert_any_call()
assert not (upload_dir / self.filename).exists()
assert not (upload_dir / self.filename_extra).exists()
def test_get_file_not_found(self, monkeypatch):
self.Storage.create_container()
file_url = f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
# Mock behavior to raise an error for missing blobs
self.Storage.container_client.get_blob_client().download_blob.side_effect = (
Exception("Blob not found")
)
with pytest.raises(Exception, match="Blob not found"):
self.Storage.get_file(file_url)
+249
View File
@@ -0,0 +1,249 @@
from contextlib import asynccontextmanager
from dataclasses import asdict, dataclass
from enum import Enum
import re
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Dict,
MutableMapping,
Optional,
cast,
)
import uuid
from asgiref.typing import (
ASGI3Application,
ASGIReceiveCallable,
ASGIReceiveEvent,
ASGISendCallable,
ASGISendEvent,
Scope as ASGIScope,
)
from loguru import logger
from starlette.requests import Request
from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE
from open_webui.utils.auth import get_current_user, get_http_authorization_cred
from open_webui.models.users import UserModel
if TYPE_CHECKING:
from loguru import Logger
@dataclass(frozen=True)
class AuditLogEntry:
# `Metadata` audit level properties
id: str
user: dict[str, Any]
audit_level: str
verb: str
request_uri: str
user_agent: Optional[str] = None
source_ip: Optional[str] = None
# `Request` audit level properties
request_object: Any = None
# `Request Response` level
response_object: Any = None
response_status_code: Optional[int] = None
class AuditLevel(str, Enum):
NONE = "NONE"
METADATA = "METADATA"
REQUEST = "REQUEST"
REQUEST_RESPONSE = "REQUEST_RESPONSE"
class AuditLogger:
"""
A helper class that encapsulates audit logging functionality. It uses Loguru’s logger with an auditable binding to ensure that audit log entries are filtered correctly.
Parameters:
logger (Logger): An instance of Loguru’s logger.
"""
def __init__(self, logger: "Logger"):
self.logger = logger.bind(auditable=True)
def write(
self,
audit_entry: AuditLogEntry,
*,
log_level: str = "INFO",
extra: Optional[dict] = None,
):
entry = asdict(audit_entry)
if extra:
entry["extra"] = extra
self.logger.log(
log_level,
"",
**entry,
)
class AuditContext:
"""
Captures and aggregates the HTTP request and response bodies during the processing of a request. It ensures that only a configurable maximum amount of data is stored to prevent excessive memory usage.
Attributes:
request_body (bytearray): Accumulated request payload.
response_body (bytearray): Accumulated response payload.
max_body_size (int): Maximum number of bytes to capture.
metadata (Dict[str, Any]): A dictionary to store additional audit metadata (user, http verb, user agent, etc.).
"""
def __init__(self, max_body_size: int = MAX_BODY_LOG_SIZE):
self.request_body = bytearray()
self.response_body = bytearray()
self.max_body_size = max_body_size
self.metadata: Dict[str, Any] = {}
def add_request_chunk(self, chunk: bytes):
if len(self.request_body) < self.max_body_size:
self.request_body.extend(
chunk[: self.max_body_size - len(self.request_body)]
)
def add_response_chunk(self, chunk: bytes):
if len(self.response_body) < self.max_body_size:
self.response_body.extend(
chunk[: self.max_body_size - len(self.response_body)]
)
class AuditLoggingMiddleware:
"""
ASGI middleware that intercepts HTTP requests and responses to perform audit logging. It captures request/response bodies (depending on audit level), headers, HTTP methods, and user information, then logs a structured audit entry at the end of the request cycle.
"""
AUDITED_METHODS = {"PUT", "PATCH", "DELETE", "POST"}
def __init__(
self,
app: ASGI3Application,
*,
excluded_paths: Optional[list[str]] = None,
max_body_size: int = MAX_BODY_LOG_SIZE,
audit_level: AuditLevel = AuditLevel.NONE,
) -> None:
self.app = app
self.audit_logger = AuditLogger(logger)
self.excluded_paths = excluded_paths or []
self.max_body_size = max_body_size
self.audit_level = audit_level
async def __call__(
self,
scope: ASGIScope,
receive: ASGIReceiveCallable,
send: ASGISendCallable,
) -> None:
if scope["type"] != "http":
return await self.app(scope, receive, send)
request = Request(scope=cast(MutableMapping, scope))
if self._should_skip_auditing(request):
return await self.app(scope, receive, send)
async with self._audit_context(request) as context:
async def send_wrapper(message: ASGISendEvent) -> None:
if self.audit_level == AuditLevel.REQUEST_RESPONSE:
await self._capture_response(message, context)
await send(message)
original_receive = receive
async def receive_wrapper() -> ASGIReceiveEvent:
nonlocal original_receive
message = await original_receive()
if self.audit_level in (
AuditLevel.REQUEST,
AuditLevel.REQUEST_RESPONSE,
):
await self._capture_request(message, context)
return message
await self.app(scope, receive_wrapper, send_wrapper)
@asynccontextmanager
async def _audit_context(
self, request: Request
) -> AsyncGenerator[AuditContext, None]:
"""
async context manager that ensures that an audit log entry is recorded after the request is processed.
"""
context = AuditContext()
try:
yield context
finally:
await self._log_audit_entry(request, context)
async def _get_authenticated_user(self, request: Request) -> UserModel:
auth_header = request.headers.get("Authorization")
assert auth_header
user = get_current_user(request, None, get_http_authorization_cred(auth_header))
return user
def _should_skip_auditing(self, request: Request) -> bool:
if (
request.method not in {"POST", "PUT", "PATCH", "DELETE"}
or AUDIT_LOG_LEVEL == "NONE"
or not request.headers.get("authorization")
):
return True
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
pattern = re.compile(
r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b"
)
if pattern.match(request.url.path):
return True
return False
async def _capture_request(self, message: ASGIReceiveEvent, context: AuditContext):
if message["type"] == "http.request":
body = message.get("body", b"")
context.add_request_chunk(body)
async def _capture_response(self, message: ASGISendEvent, context: AuditContext):
if message["type"] == "http.response.start":
context.metadata["response_status_code"] = message["status"]
elif message["type"] == "http.response.body":
body = message.get("body", b"")
context.add_response_chunk(body)
async def _log_audit_entry(self, request: Request, context: AuditContext):
try:
user = await self._get_authenticated_user(request)
entry = AuditLogEntry(
id=str(uuid.uuid4()),
user=user.model_dump(include={"id", "name", "email", "role"}),
audit_level=self.audit_level.value,
verb=request.method,
request_uri=str(request.url),
response_status_code=context.metadata.get("response_status_code", None),
source_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
request_object=context.request_body.decode("utf-8", errors="replace"),
response_object=context.response_body.decode("utf-8", errors="replace"),
)
self.audit_logger.write(entry)
except Exception as e:
logger.error(f"Failed to log audit entry: {str(e)}")
+31 -6
View File
@@ -5,6 +5,7 @@ import base64
import hmac import hmac
import hashlib import hashlib
import requests import requests
import os
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
@@ -13,15 +14,22 @@ from typing import Optional, Union, List, Dict
from open_webui.models.users import Users from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.config import override_static from open_webui.env import (
from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY WEBUI_SECRET_KEY,
TRUSTED_SIGNATURE_KEY,
STATIC_DIR,
SRC_LOG_LEVELS,
)
from fastapi import Depends, HTTPException, Request, Response, status from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from passlib.context import CryptContext from passlib.context import CryptContext
logging.getLogger("passlib").setLevel(logging.ERROR) logging.getLogger("passlib").setLevel(logging.ERROR)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OAUTH"])
SESSION_SECRET = WEBUI_SECRET_KEY SESSION_SECRET = WEBUI_SECRET_KEY
ALGORITHM = "HS256" ALGORITHM = "HS256"
@@ -47,6 +55,19 @@ def verify_signature(payload: str, signature: str) -> bool:
return False return False
def override_static(path: str, content: str):
# Ensure path is safe
if "/" in path or ".." in path:
log.error(f"Invalid path: {path}")
return
file_path = os.path.join(STATIC_DIR, path)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as f:
f.write(base64.b64decode(content)) # Convert Base64 back to raw binary
def get_license_data(app, key): def get_license_data(app, key):
if key: if key:
try: try:
@@ -69,11 +90,11 @@ def get_license_data(app, key):
return True return True
else: else:
print( log.error(
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}" f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
) )
except Exception as ex: except Exception as ex:
print(f"License: Uncaught Exception: {ex}") log.exception(f"License: Uncaught Exception: {ex}")
return False return False
@@ -129,6 +150,7 @@ def get_http_authorization_cred(auth_header: str):
def get_current_user( def get_current_user(
request: Request, request: Request,
background_tasks: BackgroundTasks,
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
): ):
token = None token = None
@@ -181,7 +203,10 @@ def get_current_user(
detail=ERROR_MESSAGES.INVALID_TOKEN, detail=ERROR_MESSAGES.INVALID_TOKEN,
) )
else: else:
Users.update_user_last_active_by_id(user.id) # Refresh the user's last active timestamp asynchronously
# to prevent blocking the request
if background_tasks:
background_tasks.add_task(Users.update_user_last_active_by_id, user.id)
return user return user
else: else:
raise HTTPException( raise HTTPException(
+5 -5
View File
@@ -66,7 +66,7 @@ async def generate_direct_chat_completion(
user: Any, user: Any,
models: dict, models: dict,
): ):
print("generate_direct_chat_completion") log.info("generate_direct_chat_completion")
metadata = form_data.pop("metadata", {}) metadata = form_data.pop("metadata", {})
@@ -103,7 +103,7 @@ async def generate_direct_chat_completion(
} }
) )
print("res", res) log.info(f"res: {res}")
if res.get("status", False): if res.get("status", False):
# Define a generator to stream responses # Define a generator to stream responses
@@ -285,7 +285,7 @@ chat_completion = generate_chat_completion
async def chat_completed(request: Request, form_data: dict, user: Any): async def chat_completed(request: Request, form_data: dict, user: Any):
if not request.app.state.MODELS: if not request.app.state.MODELS:
await get_all_models(request) await get_all_models(request, user=user)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"): if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
models = { models = {
@@ -351,7 +351,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
raise Exception(f"Action not found: {action_id}") raise Exception(f"Action not found: {action_id}")
if not request.app.state.MODELS: if not request.app.state.MODELS:
await get_all_models(request) await get_all_models(request, user=user)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"): if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
models = { models = {
@@ -432,7 +432,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
) )
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to get user values: {e}")
params = {**params, "__user__": __user__} params = {**params, "__user__": __user__}
+14 -3
View File
@@ -1,6 +1,12 @@
import inspect import inspect
import logging
from open_webui.utils.plugin import load_function_module_by_id from open_webui.utils.plugin import load_function_module_by_id
from open_webui.models.functions import Functions from open_webui.models.functions import Functions
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_sorted_filter_ids(model): def get_sorted_filter_ids(model):
@@ -61,7 +67,12 @@ async def process_filter_functions(
try: try:
# Prepare parameters # Prepare parameters
sig = inspect.signature(handler) sig = inspect.signature(handler)
params = {"body": form_data} | {
params = {"body": form_data}
if filter_type == "stream":
params = {"event": form_data}
params = params | {
k: v k: v
for k, v in { for k, v in {
**extra_params, **extra_params,
@@ -80,7 +91,7 @@ async def process_filter_functions(
) )
) )
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to get user values: {e}")
# Execute handler # Execute handler
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
@@ -89,7 +100,7 @@ async def process_filter_functions(
form_data = handler(**params) form_data = handler(**params)
except Exception as e: except Exception as e:
print(f"Error in {filter_type} handler {filter_id}: {e}") log.exception(f"Error in {filter_type} handler {filter_id}: {e}")
raise e raise e
# Handle file cleanup for inlet # Handle file cleanup for inlet
+140
View File
@@ -0,0 +1,140 @@
import json
import logging
import sys
from typing import TYPE_CHECKING
from loguru import logger
from open_webui.env import (
AUDIT_LOG_FILE_ROTATION_SIZE,
AUDIT_LOG_LEVEL,
AUDIT_LOGS_FILE_PATH,
GLOBAL_LOG_LEVEL,
)
if TYPE_CHECKING:
from loguru import Record
def stdout_format(record: "Record") -> str:
"""
Generates a formatted string for log records that are output to the console. This format includes a timestamp, log level, source location (module, function, and line), the log message, and any extra data (serialized as JSON).
Parameters:
record (Record): A Loguru record that contains logging details including time, level, name, function, line, message, and any extra context.
Returns:
str: A formatted log string intended for stdout.
"""
record["extra"]["extra_json"] = json.dumps(record["extra"])
return (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
"<level>{message}</level> - {extra[extra_json]}"
"\n{exception}"
)
class InterceptHandler(logging.Handler):
"""
Intercepts log records from Python's standard logging module
and redirects them to Loguru's logger.
"""
def emit(self, record):
"""
Called by the standard logging module for each log event.
It transforms the standard `LogRecord` into a format compatible with Loguru
and passes it to Loguru's logger.
"""
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)
def file_format(record: "Record"):
"""
Formats audit log records into a structured JSON string for file output.
Parameters:
record (Record): A Loguru record containing extra audit data.
Returns:
str: A JSON-formatted string representing the audit data.
"""
audit_data = {
"id": record["extra"].get("id", ""),
"timestamp": int(record["time"].timestamp()),
"user": record["extra"].get("user", dict()),
"audit_level": record["extra"].get("audit_level", ""),
"verb": record["extra"].get("verb", ""),
"request_uri": record["extra"].get("request_uri", ""),
"response_status_code": record["extra"].get("response_status_code", 0),
"source_ip": record["extra"].get("source_ip", ""),
"user_agent": record["extra"].get("user_agent", ""),
"request_object": record["extra"].get("request_object", b""),
"response_object": record["extra"].get("response_object", b""),
"extra": record["extra"].get("extra", {}),
}
record["extra"]["file_extra"] = json.dumps(audit_data, default=str)
return "{extra[file_extra]}\n"
def start_logger():
"""
Initializes and configures Loguru's logger with distinct handlers:
A console (stdout) handler for general log messages (excluding those marked as auditable).
An optional file handler for audit logs if audit logging is enabled.
Additionally, this function reconfigures Python’s standard logging to route through Loguru and adjusts logging levels for Uvicorn.
Parameters:
enable_audit_logging (bool): Determines whether audit-specific log entries should be recorded to file.
"""
logger.remove()
logger.add(
sys.stdout,
level=GLOBAL_LOG_LEVEL,
format=stdout_format,
filter=lambda record: "auditable" not in record["extra"],
)
if AUDIT_LOG_LEVEL != "NONE":
try:
logger.add(
AUDIT_LOGS_FILE_PATH,
level="INFO",
rotation=AUDIT_LOG_FILE_ROTATION_SIZE,
compression="zip",
format=file_format,
filter=lambda record: record["extra"].get("auditable") is True,
)
except Exception as e:
logger.error(f"Failed to initialize audit log file handler: {str(e)}")
logging.basicConfig(
handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True
)
for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]:
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
uvicorn_logger.handlers = []
for uvicorn_logger_name in ["uvicorn.access"]:
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
uvicorn_logger.handlers = [InterceptHandler()]
logger.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
+280 -187
View File
@@ -321,89 +321,95 @@ async def chat_web_search_handler(
) )
return form_data return form_data
searchQuery = queries[0] all_results = []
await event_emitter( for searchQuery in queries:
{ await event_emitter(
"type": "status", {
"data": { "type": "status",
"action": "web_search", "data": {
"description": 'Searching "{{searchQuery}}"', "action": "web_search",
"query": searchQuery, "description": 'Searching "{{searchQuery}}"',
"done": False,
},
}
)
try:
results = await process_web_search(
request,
SearchForm(
**{
"query": searchQuery, "query": searchQuery,
} "done": False,
), },
user, }
) )
if results: try:
await event_emitter( results = await process_web_search(
{ request,
"type": "status", SearchForm(
"data": { **{
"action": "web_search",
"description": "Searched {{count}} sites",
"query": searchQuery, "query": searchQuery,
"urls": results["filenames"], }
"done": True, ),
}, user=user,
}
) )
files = form_data.get("files", []) if results:
all_results.append(results)
files = form_data.get("files", [])
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT: if results.get("collection_name"):
files.append( files.append(
{ {
"docs": results.get("docs", []), "collection_name": results["collection_name"],
"name": searchQuery, "name": searchQuery,
"type": "web_search_docs", "type": "web_search",
"urls": results["filenames"], "urls": results["filenames"],
} }
) )
else: elif results.get("docs"):
files.append( files.append(
{ {
"collection_name": results["collection_name"], "docs": results.get("docs", []),
"name": searchQuery, "name": searchQuery,
"type": "web_search_results", "type": "web_search",
"urls": results["filenames"], "urls": results["filenames"],
} }
) )
form_data["files"] = files
else: form_data["files"] = files
except Exception as e:
log.exception(e)
await event_emitter( await event_emitter(
{ {
"type": "status", "type": "status",
"data": { "data": {
"action": "web_search", "action": "web_search",
"description": "No search results found", "description": 'Error searching "{{searchQuery}}"',
"query": searchQuery, "query": searchQuery,
"done": True, "done": True,
"error": True, "error": True,
}, },
} }
) )
except Exception as e:
log.exception(e) if all_results:
urls = []
for results in all_results:
if "filenames" in results:
urls.extend(results["filenames"])
await event_emitter( await event_emitter(
{ {
"type": "status", "type": "status",
"data": { "data": {
"action": "web_search", "action": "web_search",
"description": 'Error searching "{{searchQuery}}"', "description": "Searched {{count}} sites",
"query": searchQuery, "urls": urls,
"done": True,
},
}
)
else:
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "No search results found",
"done": True, "done": True,
"error": True, "error": True,
}, },
@@ -513,6 +519,7 @@ async def chat_completion_files_handler(
sources = [] sources = []
if files := body.get("metadata", {}).get("files", None): if files := body.get("metadata", {}).get("files", None):
queries = []
try: try:
queries_response = await generate_queries( queries_response = await generate_queries(
request, request,
@@ -538,8 +545,8 @@ async def chat_completion_files_handler(
queries_response = {"queries": [queries_response]} queries_response = {"queries": [queries_response]}
queries = queries_response.get("queries", []) queries = queries_response.get("queries", [])
except Exception as e: except:
queries = [] pass
if len(queries) == 0: if len(queries) == 0:
queries = [get_last_user_message(body["messages"])] queries = [get_last_user_message(body["messages"])]
@@ -551,6 +558,7 @@ async def chat_completion_files_handler(
sources = await loop.run_in_executor( sources = await loop.run_in_executor(
executor, executor,
lambda: get_sources_from_files( lambda: get_sources_from_files(
request=request,
files=files, files=files,
queries=queries, queries=queries,
embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION( embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
@@ -560,9 +568,9 @@ async def chat_completion_files_handler(
reranking_function=request.app.state.rf, reranking_function=request.app.state.rf,
r=request.app.state.config.RELEVANCE_THRESHOLD, r=request.app.state.config.RELEVANCE_THRESHOLD,
hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
full_context=request.app.state.config.RAG_FULL_CONTEXT,
), ),
) )
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
@@ -733,6 +741,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
tool_ids = form_data.pop("tool_ids", None) tool_ids = form_data.pop("tool_ids", None)
files = form_data.pop("files", None) files = form_data.pop("files", None)
# Remove files duplicates # Remove files duplicates
if files: if files:
files = list({json.dumps(f, sort_keys=True): f for f in files}.values()) files = list({json.dumps(f, sort_keys=True): f for f in files}.values())
@@ -790,8 +799,6 @@ async def process_chat_payload(request, form_data, metadata, user, model):
if len(sources) > 0: if len(sources) > 0:
context_string = "" context_string = ""
for source_idx, source in enumerate(sources): for source_idx, source in enumerate(sources):
source_id = source.get("source", {}).get("name", "")
if "document" in source: if "document" in source:
for doc_idx, doc_context in enumerate(source["document"]): for doc_idx, doc_context in enumerate(source["document"]):
context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n" context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
@@ -1043,6 +1050,21 @@ async def process_chat_response(
): ):
return response return response
extra_params = {
"__event_emitter__": event_emitter,
"__event_call__": event_caller,
"__user__": {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
},
"__metadata__": metadata,
"__request__": request,
"__model__": metadata.get("model"),
}
filter_ids = get_sorted_filter_ids(form_data.get("model"))
# Streaming response # Streaming response
if event_emitter and event_caller: if event_emitter and event_caller:
task_id = str(uuid4()) # Create a unique task ID. task_id = str(uuid4()) # Create a unique task ID.
@@ -1122,12 +1144,12 @@ async def process_chat_response(
if reasoning_duration is not None: if reasoning_duration is not None:
if raw: if raw:
content = f'{content}\n<{block["tag"]}>{block["content"]}</{block["tag"]}>\n' content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
else: else:
content = f'{content}\n<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n' content = f'{content}\n<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
else: else:
if raw: if raw:
content = f'{content}\n<{block["tag"]}>{block["content"]}</{block["tag"]}>\n' content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
else: else:
content = f'{content}\n<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n' content = f'{content}\n<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
@@ -1223,9 +1245,9 @@ async def process_chat_response(
return attributes return attributes
if content_blocks[-1]["type"] == "text": if content_blocks[-1]["type"] == "text":
for tag in tags: for start_tag, end_tag in tags:
# Match start tag e.g., <tag> or <tag attr="value"> # Match start tag e.g., <tag> or <tag attr="value">
start_tag_pattern = rf"<{tag}(\s.*?)?>" start_tag_pattern = rf"<{re.escape(start_tag)}(\s.*?)?>"
match = re.search(start_tag_pattern, content) match = re.search(start_tag_pattern, content)
if match: if match:
attr_content = ( attr_content = (
@@ -1258,7 +1280,8 @@ async def process_chat_response(
content_blocks.append( content_blocks.append(
{ {
"type": content_type, "type": content_type,
"tag": tag, "start_tag": start_tag,
"end_tag": end_tag,
"attributes": attributes, "attributes": attributes,
"content": "", "content": "",
"started_at": time.time(), "started_at": time.time(),
@@ -1270,9 +1293,10 @@ async def process_chat_response(
break break
elif content_blocks[-1]["type"] == content_type: elif content_blocks[-1]["type"] == content_type:
tag = content_blocks[-1]["tag"] start_tag = content_blocks[-1]["start_tag"]
end_tag = content_blocks[-1]["end_tag"]
# Match end tag e.g., </tag> # Match end tag e.g., </tag>
end_tag_pattern = rf"</{tag}>" end_tag_pattern = rf"<{re.escape(end_tag)}>"
# Check if the content has the end tag # Check if the content has the end tag
if re.search(end_tag_pattern, content): if re.search(end_tag_pattern, content):
@@ -1280,7 +1304,7 @@ async def process_chat_response(
block_content = content_blocks[-1]["content"] block_content = content_blocks[-1]["content"]
# Strip start and end tags from the content # Strip start and end tags from the content
start_tag_pattern = rf"<{tag}(.*?)>" start_tag_pattern = rf"<{re.escape(start_tag)}(.*?)>"
block_content = re.sub( block_content = re.sub(
start_tag_pattern, "", block_content start_tag_pattern, "", block_content
).strip() ).strip()
@@ -1345,7 +1369,7 @@ async def process_chat_response(
# Clean processed content # Clean processed content
content = re.sub( content = re.sub(
rf"<{tag}(.*?)>(.|\n)*?</{tag}>", rf"<{re.escape(start_tag)}(.*?)>(.|\n)*?<{re.escape(end_tag)}>",
"", "",
content, content,
flags=re.DOTALL, flags=re.DOTALL,
@@ -1359,7 +1383,15 @@ async def process_chat_response(
tool_calls = [] tool_calls = []
last_assistant_message = get_last_assistant_message(form_data["messages"]) last_assistant_message = None
try:
if form_data["messages"][-1]["role"] == "assistant":
last_assistant_message = get_last_assistant_message(
form_data["messages"]
)
except Exception as e:
pass
content = ( content = (
message.get("content", "") message.get("content", "")
if message if message
@@ -1375,19 +1407,24 @@ async def process_chat_response(
# We might want to disable this by default # We might want to disable this by default
DETECT_REASONING = True DETECT_REASONING = True
DETECT_SOLUTION = True
DETECT_CODE_INTERPRETER = metadata.get("features", {}).get( DETECT_CODE_INTERPRETER = metadata.get("features", {}).get(
"code_interpreter", False "code_interpreter", False
) )
reasoning_tags = [ reasoning_tags = [
"think", ("think", "/think"),
"thinking", ("thinking", "/thinking"),
"reason", ("reason", "/reason"),
"reasoning", ("reasoning", "/reasoning"),
"thought", ("thought", "/thought"),
"Thought", ("Thought", "/Thought"),
("|begin_of_thought|", "|end_of_thought|"),
] ]
code_interpreter_tags = ["code_interpreter"]
code_interpreter_tags = [("code_interpreter", "/code_interpreter")]
solution_tags = [("|begin_of_solution|", "|end_of_solution|")]
try: try:
for event in events: for event in events:
@@ -1431,119 +1468,154 @@ async def process_chat_response(
try: try:
data = json.loads(data) data = json.loads(data)
if "selected_model_id" in data: data, _ = await process_filter_functions(
model_id = data["selected_model_id"] request=request,
Chats.upsert_message_to_chat_by_id_and_message_id( filter_ids=filter_ids,
metadata["chat_id"], filter_type="stream",
metadata["message_id"], form_data=data,
{ extra_params=extra_params,
"selectedModelId": model_id, )
},
)
else:
choices = data.get("choices", [])
if not choices:
continue
delta = choices[0].get("delta", {}) if data:
delta_tool_calls = delta.get("tool_calls", None) if "selected_model_id" in data:
model_id = data["selected_model_id"]
if delta_tool_calls: Chats.upsert_message_to_chat_by_id_and_message_id(
for delta_tool_call in delta_tool_calls: metadata["chat_id"],
tool_call_index = delta_tool_call.get("index") metadata["message_id"],
{
if tool_call_index is not None: "selectedModelId": model_id,
if ( },
len(response_tool_calls)
<= tool_call_index
):
response_tool_calls.append(
delta_tool_call
)
else:
delta_name = delta_tool_call.get(
"function", {}
).get("name")
delta_arguments = delta_tool_call.get(
"function", {}
).get("arguments")
if delta_name:
response_tool_calls[
tool_call_index
]["function"]["name"] += delta_name
if delta_arguments:
response_tool_calls[
tool_call_index
]["function"][
"arguments"
] += delta_arguments
value = delta.get("content")
if value:
content = f"{content}{value}"
if not content_blocks:
content_blocks.append(
{
"type": "text",
"content": "",
}
)
content_blocks[-1]["content"] = (
content_blocks[-1]["content"] + value
) )
else:
if DETECT_REASONING: choices = data.get("choices", [])
content, content_blocks, _ = ( if not choices:
tag_content_handler( usage = data.get("usage", {})
"reasoning", if usage:
reasoning_tags, await event_emitter(
content, {
content_blocks, "type": "chat:completion",
"data": {
"usage": usage,
},
}
) )
continue
delta = choices[0].get("delta", {})
delta_tool_calls = delta.get("tool_calls", None)
if delta_tool_calls:
for delta_tool_call in delta_tool_calls:
tool_call_index = delta_tool_call.get(
"index"
)
if tool_call_index is not None:
if (
len(response_tool_calls)
<= tool_call_index
):
response_tool_calls.append(
delta_tool_call
)
else:
delta_name = delta_tool_call.get(
"function", {}
).get("name")
delta_arguments = (
delta_tool_call.get(
"function", {}
).get("arguments")
)
if delta_name:
response_tool_calls[
tool_call_index
]["function"][
"name"
] += delta_name
if delta_arguments:
response_tool_calls[
tool_call_index
]["function"][
"arguments"
] += delta_arguments
value = delta.get("content")
if value:
content = f"{content}{value}"
if not content_blocks:
content_blocks.append(
{
"type": "text",
"content": "",
}
)
content_blocks[-1]["content"] = (
content_blocks[-1]["content"] + value
) )
if DETECT_CODE_INTERPRETER: if DETECT_REASONING:
content, content_blocks, end = ( content, content_blocks, _ = (
tag_content_handler( tag_content_handler(
"code_interpreter", "reasoning",
code_interpreter_tags, reasoning_tags,
content, content,
content_blocks, content_blocks,
)
) )
)
if end: if DETECT_CODE_INTERPRETER:
break content, content_blocks, end = (
tag_content_handler(
"code_interpreter",
code_interpreter_tags,
content,
content_blocks,
)
)
if ENABLE_REALTIME_CHAT_SAVE: if end:
# Save message in the database break
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"], if DETECT_SOLUTION:
metadata["message_id"], content, content_blocks, _ = (
{ tag_content_handler(
"solution",
solution_tags,
content,
content_blocks,
)
)
if ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"content": serialize_content_blocks(
content_blocks
),
},
)
else:
data = {
"content": serialize_content_blocks( "content": serialize_content_blocks(
content_blocks content_blocks
), ),
}, }
)
else:
data = {
"content": serialize_content_blocks(
content_blocks
),
}
await event_emitter( await event_emitter(
{ {
"type": "chat:completion", "type": "chat:completion",
"data": data, "data": data,
} }
) )
except Exception as e: except Exception as e:
done = "data: [DONE]" in line done = "data: [DONE]" in line
if done: if done:
@@ -1748,6 +1820,7 @@ async def process_chat_response(
== "password" == "password"
else None else None
), ),
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
) )
else: else:
output = { output = {
@@ -1841,7 +1914,10 @@ async def process_chat_response(
} }
) )
print(content_blocks, serialize_content_blocks(content_blocks)) log.info(f"content_blocks={content_blocks}")
log.info(
f"serialize_content_blocks={serialize_content_blocks(content_blocks)}"
)
try: try:
res = await generate_chat_completion( res = await generate_chat_completion(
@@ -1912,7 +1988,7 @@ async def process_chat_response(
await background_tasks_handler() await background_tasks_handler()
except asyncio.CancelledError: except asyncio.CancelledError:
print("Task was cancelled!") log.warning("Task was cancelled!")
await event_emitter({"type": "task-cancelled"}) await event_emitter({"type": "task-cancelled"})
if not ENABLE_REALTIME_CHAT_SAVE: if not ENABLE_REALTIME_CHAT_SAVE:
@@ -1933,17 +2009,34 @@ async def process_chat_response(
return {"status": True, "task_id": task_id} return {"status": True, "task_id": task_id}
else: else:
# Fallback to the original response # Fallback to the original response
async def stream_wrapper(original_generator, events): async def stream_wrapper(original_generator, events):
def wrap_item(item): def wrap_item(item):
return f"data: {item}\n\n" return f"data: {item}\n\n"
for event in events: for event in events:
yield wrap_item(json.dumps(event)) event, _ = await process_filter_functions(
request=request,
filter_ids=filter_ids,
filter_type="stream",
form_data=event,
extra_params=extra_params,
)
if event:
yield wrap_item(json.dumps(event))
async for data in original_generator: async for data in original_generator:
yield data data, _ = await process_filter_functions(
request=request,
filter_ids=filter_ids,
filter_type="stream",
form_data=data,
extra_params=extra_params,
)
if data:
yield data
return StreamingResponse( return StreamingResponse(
stream_wrapper(response.body_iterator, events), stream_wrapper(response.body_iterator, events),
+6 -1
View File
@@ -2,12 +2,17 @@ import hashlib
import re import re
import time import time
import uuid import uuid
import logging
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
import collections.abc import collections.abc
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def deep_update(d, u): def deep_update(d, u):
@@ -412,7 +417,7 @@ def parse_ollama_modelfile(model_text):
elif param_type is bool: elif param_type is bool:
value = value.lower() == "true" value = value.lower() == "true"
except Exception as e: except Exception as e:
print(e) log.exception(f"Failed to parse parameter {param}: {e}")
continue continue
data["params"][param] = value data["params"][param] = value
+6 -5
View File
@@ -22,6 +22,7 @@ from open_webui.config import (
) )
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
from open_webui.models.users import UserModel
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
@@ -29,17 +30,17 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"]) log.setLevel(SRC_LOG_LEVELS["MAIN"])
async def get_all_base_models(request: Request): async def get_all_base_models(request: Request, user: UserModel = None):
function_models = [] function_models = []
openai_models = [] openai_models = []
ollama_models = [] ollama_models = []
if request.app.state.config.ENABLE_OPENAI_API: if request.app.state.config.ENABLE_OPENAI_API:
openai_models = await openai.get_all_models(request) openai_models = await openai.get_all_models(request, user=user)
openai_models = openai_models["data"] openai_models = openai_models["data"]
if request.app.state.config.ENABLE_OLLAMA_API: if request.app.state.config.ENABLE_OLLAMA_API:
ollama_models = await ollama.get_all_models(request) ollama_models = await ollama.get_all_models(request, user=user)
ollama_models = [ ollama_models = [
{ {
"id": model["model"], "id": model["model"],
@@ -58,8 +59,8 @@ async def get_all_base_models(request: Request):
return models return models
async def get_all_models(request): async def get_all_models(request, user: UserModel = None):
models = await get_all_base_models(request) models = await get_all_base_models(request, user=user)
# If there are no models, return an empty list # If there are no models, return an empty list
if len(models) == 0: if len(models) == 0:
+47 -16
View File
@@ -140,7 +140,14 @@ class OAuthManager:
log.debug("Running OAUTH Group management") log.debug("Running OAUTH Group management")
oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
user_oauth_groups: list[str] = user_data.get(oauth_claim, list()) # Nested claim search for groups claim
if oauth_claim:
claim_data = user_data
nested_claims = oauth_claim.split(".")
for nested_claim in nested_claims:
claim_data = claim_data.get(nested_claim, {})
user_oauth_groups = claim_data if isinstance(claim_data, list) else []
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id) user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
all_available_groups: list[GroupModel] = Groups.get_groups() all_available_groups: list[GroupModel] = Groups.get_groups()
@@ -239,11 +246,46 @@ class OAuthManager:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
provider_sub = f"{provider}@{sub}" provider_sub = f"{provider}@{sub}"
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
email = user_data.get(email_claim, "").lower() email = user_data.get(email_claim, "")
# We currently mandate that email addresses are provided # We currently mandate that email addresses are provided
if not email: if not email:
log.warning(f"OAuth callback failed, email is missing: {user_data}") # If the provider is GitHub,and public email is not provided, we can use the access token to fetch the user's email
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) if provider == "github":
try:
access_token = token.get("access_token")
headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession() as session:
async with session.get(
"https://api.github.com/user/emails", headers=headers
) as resp:
if resp.ok:
emails = await resp.json()
# use the primary email as the user's email
primary_email = next(
(e["email"] for e in emails if e.get("primary")),
None,
)
if primary_email:
email = primary_email
else:
log.warning(
"No primary email found in GitHub response"
)
raise HTTPException(
400, detail=ERROR_MESSAGES.INVALID_CRED
)
else:
log.warning("Failed to fetch GitHub email")
raise HTTPException(
400, detail=ERROR_MESSAGES.INVALID_CRED
)
except Exception as e:
log.warning(f"Error fetching GitHub email: {e}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
else:
log.warning(f"OAuth callback failed, email is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
email = email.lower()
if ( if (
"*" not in auth_manager_config.OAUTH_ALLOWED_DOMAINS "*" not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
and email.split("@")[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS and email.split("@")[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
@@ -273,21 +315,10 @@ class OAuthManager:
if not user: if not user:
user_count = Users.get_num_users() user_count = Users.get_num_users()
if (
request.app.state.USER_COUNT
and user_count >= request.app.state.USER_COUNT
):
raise HTTPException(
403,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
# If the user does not exist, check if signups are enabled # If the user does not exist, check if signups are enabled
if auth_manager_config.ENABLE_OAUTH_SIGNUP: if auth_manager_config.ENABLE_OAUTH_SIGNUP:
# Check if an existing user with the same email already exists # Check if an existing user with the same email already exists
existing_user = Users.get_user_by_email( existing_user = Users.get_user_by_email(email)
user_data.get("email", "").lower()
)
if existing_user: if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
+81 -47
View File
@@ -4,6 +4,7 @@ from open_webui.utils.misc import (
) )
from typing import Callable, Optional from typing import Callable, Optional
import json
# inplace function: form_data is modified # inplace function: form_data is modified
@@ -66,38 +67,49 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict: def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
opts = [ # Convert OpenAI parameter names to Ollama parameter names if needed.
"temperature",
"top_p",
"seed",
"mirostat",
"mirostat_eta",
"mirostat_tau",
"num_ctx",
"num_batch",
"num_keep",
"repeat_last_n",
"tfs_z",
"top_k",
"min_p",
"use_mmap",
"use_mlock",
"num_thread",
"num_gpu",
]
mappings = {i: lambda x: x for i in opts}
form_data = apply_model_params_to_body(params, form_data, mappings)
name_differences = { name_differences = {
"max_tokens": "num_predict", "max_tokens": "num_predict",
"frequency_penalty": "repeat_penalty",
} }
for key, value in name_differences.items(): for key, value in name_differences.items():
if (param := params.get(key, None)) is not None: if (param := params.get(key, None)) is not None:
form_data[value] = param # Copy the parameter to new name then delete it, to prevent Ollama warning of invalid option provided
params[value] = params[key]
del params[key]
return form_data # See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8
mappings = {
"temperature": float,
"top_p": float,
"seed": lambda x: x,
"mirostat": int,
"mirostat_eta": float,
"mirostat_tau": float,
"num_ctx": int,
"num_batch": int,
"num_keep": int,
"num_predict": int,
"repeat_last_n": int,
"top_k": int,
"min_p": float,
"typical_p": float,
"repeat_penalty": float,
"presence_penalty": float,
"frequency_penalty": float,
"penalize_newline": bool,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
"numa": bool,
"num_gpu": int,
"main_gpu": int,
"low_vram": bool,
"vocab_only": bool,
"use_mmap": bool,
"use_mlock": bool,
"num_thread": int,
}
return apply_model_params_to_body(params, form_data, mappings)
def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
@@ -108,11 +120,38 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
new_message = {"role": message["role"]} new_message = {"role": message["role"]}
content = message.get("content", []) content = message.get("content", [])
tool_calls = message.get("tool_calls", None)
tool_call_id = message.get("tool_call_id", None)
# Check if the content is a string (just a simple message) # Check if the content is a string (just a simple message)
if isinstance(content, str): if isinstance(content, str) and not tool_calls:
# If the content is a string, it's pure text # If the content is a string, it's pure text
new_message["content"] = content new_message["content"] = content
# If message is a tool call, add the tool call id to the message
if tool_call_id:
new_message["tool_call_id"] = tool_call_id
elif tool_calls:
# If tool calls are present, add them to the message
ollama_tool_calls = []
for tool_call in tool_calls:
ollama_tool_call = {
"index": tool_call.get("index", 0),
"id": tool_call.get("id", None),
"function": {
"name": tool_call.get("function", {}).get("name", ""),
"arguments": json.loads(
tool_call.get("function", {}).get("arguments", {})
),
},
}
ollama_tool_calls.append(ollama_tool_call)
new_message["tool_calls"] = ollama_tool_calls
# Put the content to empty string (Ollama requires an empty string for tool calls)
new_message["content"] = ""
else: else:
# Otherwise, assume the content is a list of dicts, e.g., text followed by an image URL # Otherwise, assume the content is a list of dicts, e.g., text followed by an image URL
content_text = "" content_text = ""
@@ -173,33 +212,28 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
ollama_payload["format"] = openai_payload["format"] ollama_payload["format"] = openai_payload["format"]
# If there are advanced parameters in the payload, format them in Ollama's options field # If there are advanced parameters in the payload, format them in Ollama's options field
ollama_options = {}
if openai_payload.get("options"): if openai_payload.get("options"):
ollama_payload["options"] = openai_payload["options"] ollama_payload["options"] = openai_payload["options"]
ollama_options = openai_payload["options"] ollama_options = openai_payload["options"]
# Handle parameters which map directly # Re-Mapping OpenAI's `max_tokens` -> Ollama's `num_predict`
for param in ["temperature", "top_p", "seed"]: if "max_tokens" in ollama_options:
if param in openai_payload: ollama_options["num_predict"] = ollama_options["max_tokens"]
ollama_options[param] = openai_payload[param] del ollama_options[
"max_tokens"
] # To prevent Ollama warning of invalid option provided
# Mapping OpenAI's `max_tokens` -> Ollama's `num_predict` # Ollama lacks a "system" prompt option. It has to be provided as a direct parameter, so we copy it down.
if "max_completion_tokens" in openai_payload: if "system" in ollama_options:
ollama_options["num_predict"] = openai_payload["max_completion_tokens"] ollama_payload["system"] = ollama_options["system"]
elif "max_tokens" in openai_payload: del ollama_options[
ollama_options["num_predict"] = openai_payload["max_tokens"] "system"
] # To prevent Ollama warning of invalid option provided
# Handle frequency / presence_penalty, which needs renaming and checking # If there is the "stop" parameter in the openai_payload, remap it to the ollama_payload.options
if "frequency_penalty" in openai_payload: if "stop" in openai_payload:
ollama_options["repeat_penalty"] = openai_payload["frequency_penalty"] ollama_options = ollama_payload.get("options", {})
ollama_options["stop"] = openai_payload.get("stop")
if "presence_penalty" in openai_payload and "penalty" not in ollama_options:
# We are assuming presence penalty uses a similar concept in Ollama, which needs custom handling if exists.
ollama_options["new_topic_penalty"] = openai_payload["presence_penalty"]
# Add options to payload if any have been set
if ollama_options:
ollama_payload["options"] = ollama_options ollama_payload["options"] = ollama_options
if "metadata" in openai_payload: if "metadata" in openai_payload:
+1 -1
View File
@@ -45,7 +45,7 @@ def extract_frontmatter(content):
frontmatter[key.strip()] = value.strip() frontmatter[key.strip()] = value.strip()
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") log.exception(f"Failed to extract frontmatter: {e}")
return {} return {}
return frontmatter return frontmatter
+33 -52
View File
@@ -24,17 +24,8 @@ def convert_ollama_tool_call_to_openai(tool_calls: dict) -> dict:
return openai_tool_calls return openai_tool_calls
def convert_response_ollama_to_openai(ollama_response: dict) -> dict: def convert_ollama_usage_to_openai(data: dict) -> dict:
model = ollama_response.get("model", "ollama") return {
message_content = ollama_response.get("message", {}).get("content", "")
tool_calls = ollama_response.get("message", {}).get("tool_calls", None)
openai_tool_calls = None
if tool_calls:
openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls)
data = ollama_response
usage = {
"response_token/s": ( "response_token/s": (
round( round(
( (
@@ -66,14 +57,42 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
"total_duration": data.get("total_duration", 0), "total_duration": data.get("total_duration", 0),
"load_duration": data.get("load_duration", 0), "load_duration": data.get("load_duration", 0),
"prompt_eval_count": data.get("prompt_eval_count", 0), "prompt_eval_count": data.get("prompt_eval_count", 0),
"prompt_tokens": int(
data.get("prompt_eval_count", 0)
), # This is the OpenAI compatible key
"prompt_eval_duration": data.get("prompt_eval_duration", 0), "prompt_eval_duration": data.get("prompt_eval_duration", 0),
"eval_count": data.get("eval_count", 0), "eval_count": data.get("eval_count", 0),
"completion_tokens": int(
data.get("eval_count", 0)
), # This is the OpenAI compatible key
"eval_duration": data.get("eval_duration", 0), "eval_duration": data.get("eval_duration", 0),
"approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")( "approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")(
(data.get("total_duration", 0) or 0) // 1_000_000_000 (data.get("total_duration", 0) or 0) // 1_000_000_000
), ),
"total_tokens": int( # This is the OpenAI compatible key
data.get("prompt_eval_count", 0) + data.get("eval_count", 0)
),
"completion_tokens_details": { # This is the OpenAI compatible key
"reasoning_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0,
},
} }
def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
model = ollama_response.get("model", "ollama")
message_content = ollama_response.get("message", {}).get("content", "")
tool_calls = ollama_response.get("message", {}).get("tool_calls", None)
openai_tool_calls = None
if tool_calls:
openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls)
data = ollama_response
usage = convert_ollama_usage_to_openai(data)
response = openai_chat_completion_message_template( response = openai_chat_completion_message_template(
model, message_content, openai_tool_calls, usage model, message_content, openai_tool_calls, usage
) )
@@ -85,7 +104,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
data = json.loads(data) data = json.loads(data)
model = data.get("model", "ollama") model = data.get("model", "ollama")
message_content = data.get("message", {}).get("content", "") message_content = data.get("message", {}).get("content", None)
tool_calls = data.get("message", {}).get("tool_calls", None) tool_calls = data.get("message", {}).get("tool_calls", None)
openai_tool_calls = None openai_tool_calls = None
@@ -96,48 +115,10 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
usage = None usage = None
if done: if done:
usage = { usage = convert_ollama_usage_to_openai(data)
"response_token/s": (
round(
(
(
data.get("eval_count", 0)
/ ((data.get("eval_duration", 0) / 10_000_000))
)
* 100
),
2,
)
if data.get("eval_duration", 0) > 0
else "N/A"
),
"prompt_token/s": (
round(
(
(
data.get("prompt_eval_count", 0)
/ ((data.get("prompt_eval_duration", 0) / 10_000_000))
)
* 100
),
2,
)
if data.get("prompt_eval_duration", 0) > 0
else "N/A"
),
"total_duration": data.get("total_duration", 0),
"load_duration": data.get("load_duration", 0),
"prompt_eval_count": data.get("prompt_eval_count", 0),
"prompt_eval_duration": data.get("prompt_eval_duration", 0),
"eval_count": data.get("eval_count", 0),
"eval_duration": data.get("eval_duration", 0),
"approximate_total": (
lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s"
)((data.get("total_duration", 0) or 0) // 1_000_000_000),
}
data = openai_chat_chunk_message_template( data = openai_chat_chunk_message_template(
model, message_content if not done else None, openai_tool_calls, usage model, message_content, openai_tool_calls, usage
) )
line = f"data: {json.dumps(data)}\n\n" line = f"data: {json.dumps(data)}\n\n"
+15 -4
View File
@@ -1,10 +1,10 @@
fastapi==0.115.7 fastapi==0.115.7
uvicorn[standard]==0.30.6 uvicorn[standard]==0.30.6
pydantic==2.9.2 pydantic==2.10.6
python-multipart==0.0.18 python-multipart==0.0.18
python-socketio==5.11.3 python-socketio==5.11.3
python-jose==3.3.0 python-jose==3.4.0
passlib[bcrypt]==1.7.4 passlib[bcrypt]==1.7.4
requests==2.32.3 requests==2.32.3
@@ -31,6 +31,9 @@ APScheduler==3.10.4
RestrictedPython==8.0 RestrictedPython==8.0
loguru==0.7.2
asgiref==3.8.1
# AI libraries # AI libraries
openai openai
anthropic anthropic
@@ -45,7 +48,7 @@ chromadb==0.6.2
pymilvus==2.5.0 pymilvus==2.5.0
qdrant-client~=1.12.0 qdrant-client~=1.12.0
opensearch-py==2.8.0 opensearch-py==2.8.0
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
transformers transformers
sentence-transformers==3.3.1 sentence-transformers==3.3.1
@@ -59,7 +62,7 @@ fpdf2==2.8.2
pymdown-extensions==10.14.2 pymdown-extensions==10.14.2
docx2txt==0.8 docx2txt==0.8
python-pptx==1.0.0 python-pptx==1.0.0
unstructured==0.16.11 unstructured==0.16.17
nltk==3.9.1 nltk==3.9.1
Markdown==3.7 Markdown==3.7
pypandoc==1.13 pypandoc==1.13
@@ -71,6 +74,7 @@ validators==0.34.0
psutil psutil
sentencepiece sentencepiece
soundfile==0.13.1 soundfile==0.13.1
azure-ai-documentintelligence==1.0.0
opencv-python-headless==4.11.0.86 opencv-python-headless==4.11.0.86
rapidocr-onnxruntime==1.3.24 rapidocr-onnxruntime==1.3.24
@@ -103,5 +107,12 @@ pytest-docker~=3.1.1
googleapis-common-protos==1.63.2 googleapis-common-protos==1.63.2
google-cloud-storage==2.19.0 google-cloud-storage==2.19.0
azure-identity==1.20.0
azure-storage-blob==12.24.1
## LDAP ## LDAP
ldap3==2.9.1 ldap3==2.9.1
## Firecrawl
firecrawl-py==1.12.0
+11
View File
@@ -3,6 +3,17 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd "$SCRIPT_DIR" || exit cd "$SCRIPT_DIR" || exit
# Add conditional Playwright browser installation
if [[ "${RAG_WEB_LOADER_ENGINE,,}" == "playwright" ]]; then
if [[ -z "${PLAYWRIGHT_WS_URI}" ]]; then
echo "Installing Playwright browsers..."
playwright install chromium
playwright install-deps chromium
fi
python -c "import nltk; nltk.download('punkt_tab')"
fi
KEY_FILE=.webui_secret_key KEY_FILE=.webui_secret_key
PORT="${PORT:-8080}" PORT="${PORT:-8080}"
+11
View File
@@ -6,6 +6,17 @@ SETLOCAL ENABLEDELAYEDEXPANSION
SET "SCRIPT_DIR=%~dp0" SET "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%" || exit /b cd /d "%SCRIPT_DIR%" || exit /b
:: Add conditional Playwright browser installation
IF /I "%RAG_WEB_LOADER_ENGINE%" == "playwright" (
IF "%PLAYWRIGHT_WS_URI%" == "" (
echo Installing Playwright browsers...
playwright install chromium
playwright install-deps chromium
)
python -c "import nltk; nltk.download('punkt_tab')"
)
SET "KEY_FILE=.webui_secret_key" SET "KEY_FILE=.webui_secret_key"
IF "%PORT%"=="" SET PORT=8080 IF "%PORT%"=="" SET PORT=8080
IF "%HOST%"=="" SET HOST=0.0.0.0 IF "%HOST%"=="" SET HOST=0.0.0.0
+10
View File
@@ -0,0 +1,10 @@
services:
playwright:
image: mcr.microsoft.com/playwright:v1.49.1-noble # Version must match requirements.txt
container_name: playwright
command: npx -y playwright@1.49.1 run-server --port 3000 --host 0.0.0.0
open-webui:
environment:
- 'RAG_WEB_LOADER_ENGINE=playwright'
- 'PLAYWRIGHT_WS_URI=ws://playwright:3000'
+223 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.5.13", "version": "0.5.18",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.5.13", "version": "0.5.18",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
@@ -41,6 +41,7 @@
"i18next-resources-to-backend": "^1.2.0", "i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1", "idb": "^7.1.1",
"js-sha256": "^0.10.1", "js-sha256": "^0.10.1",
"jspdf": "^3.0.0",
"katex": "^0.16.21", "katex": "^0.16.21",
"kokoro-js": "^1.1.1", "kokoro-js": "^1.1.1",
"marked": "^9.1.0", "marked": "^9.1.0",
@@ -63,6 +64,7 @@
"svelte-sonner": "^0.3.19", "svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"undici": "^7.3.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite-plugin-static-copy": "^2.2.0" "vite-plugin-static-copy": "^2.2.0"
}, },
@@ -134,9 +136,10 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.24.1", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
"license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -3169,6 +3172,13 @@
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true "dev": true
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -3198,6 +3208,13 @@
"integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==",
"dev": true "dev": true
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "2.0.10", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
@@ -3793,6 +3810,18 @@
"node": ">= 4.0.0" "node": ">= 4.0.0"
} }
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/aws-sign2": { "node_modules/aws-sign2": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -3828,6 +3857,16 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4032,6 +4071,18 @@
"node": "10.* || >= 12.*" "node": "10.* || >= 12.*"
} }
}, },
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -4130,6 +4181,33 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/canvg/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/caseless": { "node_modules/caseless": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@@ -4598,6 +4676,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4642,6 +4732,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -6105,6 +6205,12 @@
"pend": "~1.2.0" "pend": "~1.2.0"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/figures": { "node_modules/figures": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -6700,6 +6806,20 @@
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -7212,6 +7332,34 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jspdf": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.0.tgz",
"integrity": "sha512-QvuQZvOI8CjfjVgtajdL0ihrDYif1cN5gXiF9lb9Pd9JOpmocvnNyFO9sdiJ/8RA5Bu8zyGOUjJLj5kiku16ug==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf/node_modules/dompurify": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/jsprim": { "node_modules/jsprim": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -9028,7 +9176,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"dev": true "devOptional": true
}, },
"node_modules/periscopic": { "node_modules/periscopic": {
"version": "3.1.0", "version": "3.1.0",
@@ -9754,6 +9902,16 @@
"rimraf": "bin.js" "rimraf": "bin.js"
} }
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -9893,6 +10051,16 @@
"integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
"dev": true "dev": true
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -10784,6 +10952,16 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true "dev": true
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/std-env": { "node_modules/std-env": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
@@ -11165,6 +11343,16 @@
"@types/estree": "*" "@types/estree": "*"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/symlink-or-copy": { "node_modules/symlink-or-copy": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
@@ -11257,6 +11445,16 @@
"streamx": "^2.12.5" "streamx": "^2.12.5"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -11528,6 +11726,15 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/undici": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.3.0.tgz",
"integrity": "sha512-Qy96NND4Dou5jKoSJ2gm8ax8AJM/Ey9o9mz7KN1bb9GP+G0l20Zw8afxTnY2f4b7hmhn/z8aC2kfArVQlAhFBw==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -11587,6 +11794,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.5.13", "version": "0.5.18",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
@@ -84,6 +84,7 @@
"i18next-resources-to-backend": "^1.2.0", "i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1", "idb": "^7.1.1",
"js-sha256": "^0.10.1", "js-sha256": "^0.10.1",
"jspdf": "^3.0.0",
"katex": "^0.16.21", "katex": "^0.16.21",
"kokoro-js": "^1.1.1", "kokoro-js": "^1.1.1",
"marked": "^9.1.0", "marked": "^9.1.0",
@@ -106,6 +107,7 @@
"svelte-sonner": "^0.3.19", "svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"undici": "^7.3.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite-plugin-static-copy": "^2.2.0" "vite-plugin-static-copy": "^2.2.0"
}, },
+14 -3
View File
@@ -8,11 +8,11 @@ license = { file = "LICENSE" }
dependencies = [ dependencies = [
"fastapi==0.115.7", "fastapi==0.115.7",
"uvicorn[standard]==0.30.6", "uvicorn[standard]==0.30.6",
"pydantic==2.9.2", "pydantic==2.10.6",
"python-multipart==0.0.18", "python-multipart==0.0.18",
"python-socketio==5.11.3", "python-socketio==5.11.3",
"python-jose==3.3.0", "python-jose==3.4.0",
"passlib[bcrypt]==1.7.4", "passlib[bcrypt]==1.7.4",
"requests==2.32.3", "requests==2.32.3",
@@ -40,6 +40,9 @@ dependencies = [
"RestrictedPython==8.0", "RestrictedPython==8.0",
"loguru==0.7.2",
"asgiref==3.8.1",
"openai", "openai",
"anthropic", "anthropic",
"google-generativeai==0.7.2", "google-generativeai==0.7.2",
@@ -53,6 +56,7 @@ dependencies = [
"pymilvus==2.5.0", "pymilvus==2.5.0",
"qdrant-client~=1.12.0", "qdrant-client~=1.12.0",
"opensearch-py==2.8.0", "opensearch-py==2.8.0",
"playwright==1.49.1",
"transformers", "transformers",
"sentence-transformers==3.3.1", "sentence-transformers==3.3.1",
@@ -65,7 +69,7 @@ dependencies = [
"pymdown-extensions==10.14.2", "pymdown-extensions==10.14.2",
"docx2txt==0.8", "docx2txt==0.8",
"python-pptx==1.0.0", "python-pptx==1.0.0",
"unstructured==0.16.11", "unstructured==0.16.17",
"nltk==3.9.1", "nltk==3.9.1",
"Markdown==3.7", "Markdown==3.7",
"pypandoc==1.13", "pypandoc==1.13",
@@ -77,6 +81,7 @@ dependencies = [
"psutil", "psutil",
"sentencepiece", "sentencepiece",
"soundfile==0.13.1", "soundfile==0.13.1",
"azure-ai-documentintelligence==1.0.0",
"opencv-python-headless==4.11.0.86", "opencv-python-headless==4.11.0.86",
"rapidocr-onnxruntime==1.3.24", "rapidocr-onnxruntime==1.3.24",
@@ -108,7 +113,13 @@ dependencies = [
"googleapis-common-protos==1.63.2", "googleapis-common-protos==1.63.2",
"google-cloud-storage==2.19.0", "google-cloud-storage==2.19.0",
"azure-identity==1.20.0",
"azure-storage-blob==12.24.1",
"ldap3==2.9.1", "ldap3==2.9.1",
"firecrawl-py==1.12.0",
"gcp-storage-emulator>=2024.8.3", "gcp-storage-emulator>=2024.8.3",
] ]
readme = "README.md" readme = "README.md"
+9
View File
@@ -74,6 +74,7 @@ usage() {
echo " --enable-api[port=PORT] Enable API and expose it on the specified port." echo " --enable-api[port=PORT] Enable API and expose it on the specified port."
echo " --webui[port=PORT] Set the port for the web user interface." echo " --webui[port=PORT] Set the port for the web user interface."
echo " --data[folder=PATH] Bind mount for ollama data folder (by default will create the 'ollama' volume)." echo " --data[folder=PATH] Bind mount for ollama data folder (by default will create the 'ollama' volume)."
echo " --playwright Enable Playwright support for web scraping."
echo " --build Build the docker image before running the compose project." echo " --build Build the docker image before running the compose project."
echo " --drop Drop the compose project." echo " --drop Drop the compose project."
echo " -q, --quiet Run script in headless mode." echo " -q, --quiet Run script in headless mode."
@@ -100,6 +101,7 @@ webui_port=3000
headless=false headless=false
build_image=false build_image=false
kill_compose=false kill_compose=false
enable_playwright=false
# Function to extract value from the parameter # Function to extract value from the parameter
extract_value() { extract_value() {
@@ -129,6 +131,9 @@ while [[ $# -gt 0 ]]; do
value=$(extract_value "$key") value=$(extract_value "$key")
data_dir=${value:-"./ollama-data"} data_dir=${value:-"./ollama-data"}
;; ;;
--playwright)
enable_playwright=true
;;
--drop) --drop)
kill_compose=true kill_compose=true
;; ;;
@@ -182,6 +187,9 @@ else
DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml" DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml"
export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
fi fi
if [[ $enable_playwright == true ]]; then
DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.playwright.yaml"
fi
if [[ -n $webui_port ]]; then if [[ -n $webui_port ]]; then
export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable
fi fi
@@ -201,6 +209,7 @@ echo -e " ${GREEN}${BOLD}GPU Count:${NC} ${OLLAMA_GPU_COUNT:-Not Enabled}"
echo -e " ${GREEN}${BOLD}WebAPI Port:${NC} ${OLLAMA_WEBAPI_PORT:-Not Enabled}" echo -e " ${GREEN}${BOLD}WebAPI Port:${NC} ${OLLAMA_WEBAPI_PORT:-Not Enabled}"
echo -e " ${GREEN}${BOLD}Data Folder:${NC} ${data_dir:-Using ollama volume}" echo -e " ${GREEN}${BOLD}Data Folder:${NC} ${data_dir:-Using ollama volume}"
echo -e " ${GREEN}${BOLD}WebUI Port:${NC} $webui_port" echo -e " ${GREEN}${BOLD}WebUI Port:${NC} $webui_port"
echo -e " ${GREEN}${BOLD}Playwright:${NC} ${enable_playwright:-false}"
echo echo
if [[ $headless == true ]]; then if [[ $headless == true ]]; then
+32
View File
@@ -16,8 +16,39 @@ const packages = [
]; ];
import { loadPyodide } from 'pyodide'; import { loadPyodide } from 'pyodide';
import { setGlobalDispatcher, ProxyAgent } from 'undici';
import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises'; import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises';
/**
* Loading network proxy configurations from the environment variables.
* And the proxy config with lowercase name has the highest priority to use.
*/
function initNetworkProxyFromEnv() {
// we assume all subsequent requests in this script are HTTPS:
// https://cdn.jsdelivr.net
// https://pypi.org
// https://files.pythonhosted.org
const allProxy = process.env.all_proxy || process.env.ALL_PROXY;
const httpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
const httpProxy = process.env.http_proxy || process.env.HTTP_PROXY;
const preferedProxy = httpsProxy || allProxy || httpProxy;
/**
* use only http(s) proxy because socks5 proxy is not supported currently:
* @see https://github.com/nodejs/undici/issues/2224
*/
if (!preferedProxy || !preferedProxy.startsWith('http')) return;
let preferedProxyURL;
try {
preferedProxyURL = new URL(preferedProxy).toString();
} catch {
console.warn(`Invalid network proxy URL: "${preferedProxy}"`);
return;
}
const dispatcher = new ProxyAgent({ uri: preferedProxyURL });
setGlobalDispatcher(dispatcher);
console.log(`Initialized network proxy "${preferedProxy}" from env`);
}
async function downloadPackages() { async function downloadPackages() {
console.log('Setting up pyodide + micropip'); console.log('Setting up pyodide + micropip');
@@ -84,5 +115,6 @@ async function copyPyodide() {
} }
} }
initNetworkProxyFromEnv();
await downloadPackages(); await downloadPackages();
await copyPyodide(); await copyPyodide();
+1 -1
View File
@@ -101,7 +101,7 @@ li p {
/* Dark theme scrollbar styles */ /* Dark theme scrollbar styles */
.dark ::-webkit-scrollbar-thumb { .dark ::-webkit-scrollbar-thumb {
background-color: rgba(33, 33, 33, 0.8); /* Darker color for dark theme */ background-color: rgba(42, 42, 42, 0.8); /* Darker color for dark theme */
border-color: rgba(0, 0, 0, var(--tw-border-opacity)); border-color: rgba(0, 0, 0, var(--tw-border-opacity));
} }
+7
View File
@@ -32,9 +32,15 @@ type ChunkConfigForm = {
chunk_overlap: number; chunk_overlap: number;
}; };
type DocumentIntelligenceConfigForm = {
key: string;
endpoint: string;
};
type ContentExtractConfigForm = { type ContentExtractConfigForm = {
engine: string; engine: string;
tika_server_url: string | null; tika_server_url: string | null;
document_intelligence_config: DocumentIntelligenceConfigForm | null;
}; };
type YoutubeConfigForm = { type YoutubeConfigForm = {
@@ -46,6 +52,7 @@ type YoutubeConfigForm = {
type RAGConfigForm = { type RAGConfigForm = {
pdf_extract_images?: boolean; pdf_extract_images?: boolean;
enable_google_drive_integration?: boolean; enable_google_drive_integration?: boolean;
enable_onedrive_integration?: boolean;
chunk?: ChunkConfigForm; chunk?: ChunkConfigForm;
content_extraction?: ContentExtractConfigForm; content_extraction?: ContentExtractConfigForm;
web_loader_ssl_verification?: boolean; web_loader_ssl_verification?: boolean;
@@ -1,8 +1,7 @@
<script> <script>
import { getContext, createEventDispatcher, onMount, tick } from 'svelte'; import { getContext, onMount, tick } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
import CodeEditor from '$lib/components/common/CodeEditor.svelte'; import CodeEditor from '$lib/components/common/CodeEditor.svelte';
@@ -15,6 +14,8 @@
let loading = false; let loading = false;
let showConfirm = false; let showConfirm = false;
export let onSave = () => {};
export let edit = false; export let edit = false;
export let clone = false; export let clone = false;
@@ -256,7 +257,7 @@ class Pipe:
const saveHandler = async () => { const saveHandler = async () => {
loading = true; loading = true;
dispatch('save', { onSave({
id, id,
name, name,
meta, meta,
@@ -371,10 +372,10 @@ class Pipe:
value={content} value={content}
lang="python" lang="python"
{boilerplate} {boilerplate}
on:change={(e) => { onChange={(e) => {
_content = e.detail.value; _content = e;
}} }}
on:save={async () => { onSave={async () => {
if (formElement) { if (formElement) {
formElement.requestSubmit(); formElement.requestSubmit();
} }
@@ -91,45 +91,65 @@
</div> </div>
</div> </div>
<div class=" flex gap-2 w-full items-center justify-between"> <div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="text-xs font-medium"> <div class=" flex gap-2 w-full items-center justify-between">
{$i18n.t('Jupyter Auth')} <div class="text-xs font-medium">
</div> {$i18n.t('Jupyter Auth')}
</div>
<div> <div>
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH} bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')} placeholder={$i18n.t('Select an auth method')}
> >
<option selected value="">{$i18n.t('None')}</option> <option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option> <option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option> <option value="password">{$i18n.t('Password')}</option>
</select> </select>
</div>
</div>
{#if config.CODE_EXECUTION_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div> </div>
</div> </div>
{/if}
{#if config.CODE_EXECUTION_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex gap-2 w-full items-center justify-between">
<div class="text-xs font-medium">
{$i18n.t('Code Execution Timeout')}
</div>
<div class="">
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
<input
class="dark:bg-gray-900 w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
type="number"
bind:value={config.CODE_EXECUTION_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
{/if} {/if}
</div> </div>
@@ -197,45 +217,65 @@
</div> </div>
</div> </div>
<div class="flex gap-2 w-full items-center justify-between"> <div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="text-xs font-medium"> <div class="flex gap-2 w-full items-center justify-between">
{$i18n.t('Jupyter Auth')} <div class="text-xs font-medium">
</div> {$i18n.t('Jupyter Auth')}
</div>
<div> <div>
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH} bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')} placeholder={$i18n.t('Select an auth method')}
> >
<option selected value="">{$i18n.t('None')}</option> <option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option> <option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option> <option value="password">{$i18n.t('Password')}</option>
</select> </select>
</div>
</div>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div> </div>
</div> </div>
{/if}
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex gap-2 w-full items-center justify-between">
<div class="text-xs font-medium">
{$i18n.t('Code Execution Timeout')}
</div>
<div class="">
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
<input
class="dark:bg-gray-900 w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
type="number"
bind:value={config.CODE_INTERPRETER_JUPYTER_TIMEOUT}
placeholder={$i18n.t('e.g. 60')}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
{/if} {/if}
<hr class="border-gray-100 dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
@@ -274,6 +274,7 @@
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
}); });
OPENAI_API_CONFIGS = newConfig; OPENAI_API_CONFIGS = newConfig;
updateOpenAIHandler();
}} }}
/> />
{/each} {/each}
File diff suppressed because it is too large. Load diff
@@ -103,10 +103,12 @@
<div class="overflow-y-scroll scrollbar-hidden h-full"> <div class="overflow-y-scroll scrollbar-hidden h-full">
{#if evaluationConfig !== null} {#if evaluationConfig !== null}
<div class=""> <div class="">
<div class="text-sm font-medium mb-2">{$i18n.t('General Settings')}</div> <div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mb-2"> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="flex justify-between items-center text-xs">
<div class="mb-2.5 flex w-full justify-between">
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div> <div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}> <Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
@@ -116,46 +118,50 @@
</div> </div>
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} {#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
<hr class=" border-gray-50 dark:border-gray-700/10 my-2" /> <div class="mb-3">
<div class=" mb-2.5 text-base font-medium flex justify-between items-center">
<div class="flex justify-between items-center mb-2"> <div>
<div class="text-sm font-medium">{$i18n.t('Manage Arena Models')}</div> {$i18n.t('Manage')}
<div>
<Tooltip content={$i18n.t('Add Arena Model')}>
<button
class="p-1"
type="button"
on:click={() => {
showAddModel = true;
}}
>
<Plus />
</button>
</Tooltip>
</div>
</div>
<div class="flex flex-col gap-2">
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
<Model
{model}
on:edit={(e) => {
editModelHandler(e.detail, index);
}}
on:delete={(e) => {
deleteModelHandler(index);
}}
/>
{/each}
{:else}
<div class=" text-center text-xs text-gray-500">
{$i18n.t(
`Using the default arena model with all models. Click the plus button to add custom models.`
)}
</div> </div>
{/if}
<div>
<Tooltip content={$i18n.t('Add Arena Model')}>
<button
class="p-1"
type="button"
on:click={() => {
showAddModel = true;
}}
>
<Plus />
</button>
</Tooltip>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="flex flex-col gap-2">
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
<Model
{model}
on:edit={(e) => {
editModelHandler(e.detail, index);
}}
on:delete={(e) => {
deleteModelHandler(index);
}}
/>
{/each}
{:else}
<div class=" text-center text-xs text-gray-500">
{$i18n.t(
`Using the default arena model with all models. Click the plus button to add custom models.`
)}
</div>
{/if}
</div>
</div> </div>
{/if} {/if}
</div> </div>
@@ -261,6 +261,9 @@
} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') { } else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
toast.error($i18n.t('OpenAI API Key is required.')); toast.error($i18n.t('OpenAI API Key is required.'));
config.enabled = false; config.enabled = false;
} else if (config.engine === 'gemini' && config.gemini.GEMINI_API_KEY === '') {
toast.error($i18n.t('Gemini API Key is required.'));
config.enabled = false;
} }
} }
@@ -294,6 +297,7 @@
<option value="openai">{$i18n.t('Default (Open AI)')}</option> <option value="openai">{$i18n.t('Default (Open AI)')}</option>
<option value="comfyui">{$i18n.t('ComfyUI')}</option> <option value="comfyui">{$i18n.t('ComfyUI')}</option>
<option value="automatic1111">{$i18n.t('Automatic1111')}</option> <option value="automatic1111">{$i18n.t('Automatic1111')}</option>
<option value="gemini">{$i18n.t('Gemini')}</option>
</select> </select>
</div> </div>
</div> </div>
@@ -605,6 +609,24 @@
/> />
</div> </div>
</div> </div>
{:else if config?.engine === 'gemini'}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
<div class="flex gap-2 mb-1">
<input
class="flex-1 w-full text-sm bg-transparent outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={config.gemini.GEMINI_API_BASE_URL}
required
/>
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={config.gemini.GEMINI_API_KEY}
/>
</div>
</div>
{/if} {/if}
</div> </div>
@@ -51,7 +51,7 @@
onMount(async () => { onMount(async () => {
taskConfig = await getTaskConfig(localStorage.token); taskConfig = await getTaskConfig(localStorage.token);
promptSuggestions = $config?.default_prompt_suggestions; promptSuggestions = $config?.default_prompt_suggestions ?? [];
banners = await getBanners(localStorage.token); banners = await getBanners(localStorage.token);
}); });
@@ -16,6 +16,8 @@
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Minus from '$lib/components/icons/Minus.svelte'; import Minus from '$lib/components/icons/Minus.svelte';
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
export let show = false; export let show = false;
export let initHandler = () => {}; export let initHandler = () => {};
@@ -26,6 +28,9 @@
let defaultModelIds = []; let defaultModelIds = [];
let modelIds = []; let modelIds = [];
let sortKey = '';
let sortOrder = '';
let loading = false; let loading = false;
let showResetModal = false; let showResetModal = false;
@@ -71,6 +76,9 @@
// Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically // Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b)) ...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
]; ];
sortKey = '';
sortOrder = '';
}; };
const submitHandler = async () => { const submitHandler = async () => {
loading = true; loading = true;
@@ -145,9 +153,45 @@
> >
<div> <div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="mb-1 flex justify-between"> <button
class="mb-1 flex gap-2"
type="button"
on:click={() => {
sortKey = 'model';
if (sortOrder === 'asc') {
sortOrder = 'desc';
} else {
sortOrder = 'asc';
}
modelIds = modelIds
.filter((id) => id !== '')
.sort((a, b) => {
const nameA = $models.find((model) => model.id === a)?.name || a;
const nameB = $models.find((model) => model.id === b)?.name || b;
return sortOrder === 'desc'
? nameA.localeCompare(nameB)
: nameB.localeCompare(nameA);
});
}}
>
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div> <div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
</div>
{#if sortKey === 'model'}
<span class="font-normal self-center">
{#if sortOrder === 'asc'}
<ChevronUp className="size-3" />
{:else}
<ChevronDown className="size-3" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-3" />
</span>
{/if}
</button>
<ModelList bind:modelIds /> <ModelList bind:modelIds />
</div> </div>
@@ -21,14 +21,24 @@
modelIds = modelList; modelIds = modelList;
}; };
onMount(() => { $: if (modelIds) {
sortable = Sortable.create(modelListElement, { init();
animation: 150, }
onUpdate: async (event) => {
positionChangeHandler(); const init = () => {
} if (sortable) {
}); sortable.destroy();
}); }
if (modelListElement) {
sortable = Sortable.create(modelListElement, {
animation: 150,
onUpdate: async (event) => {
positionChangeHandler();
}
});
}
};
</script> </script>
{#if modelIds.length > 0} {#if modelIds.length > 0}
+324 -292
View File
@@ -85,384 +85,416 @@
> >
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if webConfig} {#if webConfig}
<div> <div class="">
<div class=" mb-1 text-sm font-medium"> <div class="mb-3">
{$i18n.t('Web Search')} <div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
</div>
<div> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" py-0.5 flex w-full justify-between">
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Enable Web Search')} {$i18n.t('Web Search')}
</div>
<div class="flex items-center relative">
<Switch bind:state={webConfig.search.enabled} />
</div> </div>
<Switch bind:state={webConfig.search.enabled} />
</div> </div>
</div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div> <div class=" self-center text-xs font-medium">
<div class="flex items-center relative"> {$i18n.t('Web Search Engine')}
<select </div>
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" <div class="flex items-center relative">
bind:value={webConfig.search.engine} <select
placeholder={$i18n.t('Select a engine')} class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
required bind:value={webConfig.search.engine}
> placeholder={$i18n.t('Select a engine')}
<option disabled selected value="">{$i18n.t('Select a engine')}</option> required
{#each webSearchEngines as engine} >
<option value={engine}>{engine}</option> <option disabled selected value="">{$i18n.t('Select a engine')}</option>
{/each} {#each webSearchEngines as engine}
</select> <option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div> </div>
</div>
<div class=" py-0.5 flex w-full justify-between"> {#if webConfig.search.engine !== ''}
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT
? 'Inject the entire web results as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT} />
</Tooltip>
</div>
</div>
{#if webConfig.search.engine !== ''}
<div class="mt-1.5">
{#if webConfig.search.engine === 'searxng'} {#if webConfig.search.engine === 'searxng'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Searxng Query URL')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Searxng Query URL')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter Searxng Query URL')} placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url} bind:value={webConfig.search.searxng_query_url}
autocomplete="off" autocomplete="off"
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
{:else if webConfig.search.engine === 'google_pse'} {:else if webConfig.search.engine === 'google_pse'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Google PSE API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Google PSE API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Google PSE API Key')} placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key} bind:value={webConfig.search.google_pse_api_key}
/> />
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE Engine Id')}
</div> </div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE Engine Id')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')} placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.search.google_pse_engine_id} bind:value={webConfig.search.google_pse_engine_id}
autocomplete="off" autocomplete="off"
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
{:else if webConfig.search.engine === 'brave'} {:else if webConfig.search.engine === 'brave'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Brave Search API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Brave Search API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Brave Search API Key')} placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key} bind:value={webConfig.search.brave_search_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'kagi'} {:else if webConfig.search.engine === 'kagi'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Kagi Search API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Kagi Search API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Kagi Search API Key')} placeholder={$i18n.t('Enter Kagi Search API Key')}
bind:value={webConfig.search.kagi_search_api_key} bind:value={webConfig.search.kagi_search_api_key}
/> />
</div>
.
</div> </div>
{:else if webConfig.search.engine === 'mojeek'} {:else if webConfig.search.engine === 'mojeek'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Mojeek Search API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Mojeek Search API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Mojeek Search API Key')} placeholder={$i18n.t('Enter Mojeek Search API Key')}
bind:value={webConfig.search.mojeek_search_api_key} bind:value={webConfig.search.mojeek_search_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'bocha'} {:else if webConfig.search.engine === 'bocha'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Bocha Search API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Bocha Search API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Bocha Search API Key')} placeholder={$i18n.t('Enter Bocha Search API Key')}
bind:value={webConfig.search.bocha_search_api_key} bind:value={webConfig.search.bocha_search_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'serpstack'} {:else if webConfig.search.engine === 'serpstack'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Serpstack API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Serpstack API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Serpstack API Key')} placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key} bind:value={webConfig.search.serpstack_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'serper'} {:else if webConfig.search.engine === 'serper'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Serper API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Serper API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Serper API Key')} placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key} bind:value={webConfig.search.serper_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'serply'} {:else if webConfig.search.engine === 'serply'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Serply API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Serply API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Serply API Key')} placeholder={$i18n.t('Enter Serply API Key')}
bind:value={webConfig.search.serply_api_key} bind:value={webConfig.search.serply_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'searchapi'} {:else if webConfig.search.engine === 'searchapi'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('SearchApi API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('SearchApi API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter SearchApi API Key')} placeholder={$i18n.t('Enter SearchApi API Key')}
bind:value={webConfig.search.searchapi_api_key} bind:value={webConfig.search.searchapi_api_key}
/> />
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SearchApi Engine')}
</div> </div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SearchApi Engine')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter SearchApi Engine')} placeholder={$i18n.t('Enter SearchApi Engine')}
bind:value={webConfig.search.searchapi_engine} bind:value={webConfig.search.searchapi_engine}
autocomplete="off" autocomplete="off"
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
{:else if webConfig.search.engine === 'serpapi'} {:else if webConfig.search.engine === 'serpapi'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('SerpApi API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('SerpApi API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter SerpApi API Key')} placeholder={$i18n.t('Enter SerpApi API Key')}
bind:value={webConfig.search.serpapi_api_key} bind:value={webConfig.search.serpapi_api_key}
/> />
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SerpApi Engine')}
</div> </div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SerpApi Engine')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter SerpApi Engine')} placeholder={$i18n.t('Enter SerpApi Engine')}
bind:value={webConfig.search.serpapi_engine} bind:value={webConfig.search.serpapi_engine}
autocomplete="off" autocomplete="off"
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
{:else if webConfig.search.engine === 'tavily'} {:else if webConfig.search.engine === 'tavily'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Tavily API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Tavily API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')} placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key} bind:value={webConfig.search.tavily_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'jina'} {:else if webConfig.search.engine === 'jina'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Jina API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Jina API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Jina API Key')} placeholder={$i18n.t('Enter Jina API Key')}
bind:value={webConfig.search.jina_api_key} bind:value={webConfig.search.jina_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'exa'} {:else if webConfig.search.engine === 'exa'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Exa API Key')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Exa API Key')}
</div>
<SensitiveInput <SensitiveInput
placeholder={$i18n.t('Enter Exa API Key')} placeholder={$i18n.t('Enter Exa API Key')}
bind:value={webConfig.search.exa_api_key} bind:value={webConfig.search.exa_api_key}
/> />
</div>
</div> </div>
{:else if webConfig.search.engine === 'bing'} {:else if webConfig.search.engine === 'bing'}
<div> <div class="mb-2.5 flex w-full flex-col">
<div class=" self-center text-xs font-medium mb-1"> <div>
{$i18n.t('Bing Search V7 Endpoint')} <div class=" self-center text-xs font-medium mb-1">
</div> {$i18n.t('Bing Search V7 Endpoint')}
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')} placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.search.bing_search_v7_endpoint} bind:value={webConfig.search.bing_search_v7_endpoint}
autocomplete="off" autocomplete="off"
/> />
</div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<div class=" self-center text-xs font-medium mb-1"> <div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Subscription Key')} {$i18n.t('Bing Search V7 Subscription Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
bind:value={webConfig.search.bing_search_v7_subscription_key}
/>
</div> </div>
<SensitiveInput
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
bind:value={webConfig.search.bing_search_v7_subscription_key}
/>
</div> </div>
{/if} {/if}
</div> {/if}
{/if}
{#if webConfig.search.enabled} {#if webConfig.search.enabled}
<div class="mt-2 flex gap-2 mb-1"> <div class="mb-2.5 flex w-full flex-col">
<div class="w-full"> <div class="flex gap-2">
<div class=" self-center text-xs font-medium mb-1"> <div class="w-full">
{$i18n.t('Search Result Count')} <div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Search Result Count')}
</div>
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.search.result_count}
required
/>
</div>
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Concurrent Requests')}
</div>
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests}
required
/>
</div>
</div>
</div>
<div class="mb-2.5 flex w-full flex-col">
<div class=" text-xs font-medium mb-1">
{$i18n.t('Domain Filter List')}
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Search Result Count')} placeholder={$i18n.t(
bind:value={webConfig.search.result_count} 'Enter domains separated by commas (e.g., example.com,site.org)'
required )}
bind:value={webConfig.search.domain_filter_list}
/> />
</div> </div>
{/if}
<div class="w-full"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium mb-1"> <div class=" self-center text-xs font-medium">
{$i18n.t('Concurrent Requests')} <Tooltip content={$i18n.t('Full Context Mode')} placement="top-start">
</div> {$i18n.t('Bypass Embedding and Retrieval')}
</Tooltip>
<input </div>
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" <div class="flex items-center relative">
placeholder={$i18n.t('Concurrent Requests')} <Tooltip
bind:value={webConfig.search.concurrent_requests} content={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
required ? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
/> : 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL} />
</Tooltip>
</div> </div>
</div> </div>
<div class="mt-2"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium mb-1"> <div class=" self-center text-xs font-medium">
{$i18n.t('Domain Filter List')} {$i18n.t('Trust Proxy Environment')}
</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.search.trust_env
? 'Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents'
: 'Use no proxy to fetch page contents.'}
>
<Switch bind:state={webConfig.search.trust_env} />
</Tooltip>
</div> </div>
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org)'
)}
bind:value={webConfig.search.domain_filter_list}
/>
</div> </div>
{/if}
</div>
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Web Loader Settings')}
</div> </div>
<div> <div class="mb-3">
<div class=" py-0.5 flex w-full justify-between"> <div class=" mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Bypass SSL verification for Websites')} {$i18n.t('Bypass SSL verification for Websites')}
</div> </div>
<div class="flex items-center relative">
<button <Switch bind:state={webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION} />
class="p-1 px-3 text-xs flex rounded-sm transition" </div>
on:click={() => {
webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION =
!webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION;
submitHandler();
}}
type="button"
>
{#if webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION === false}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div> </div>
</div>
<div class=" mt-2 mb-1 text-sm font-medium"> <div class=" mb-2.5 flex w-full justify-between">
{$i18n.t('Youtube Loader Settings')} <div class=" self-center text-xs font-medium">
</div> {$i18n.t('Youtube Language')}
</div>
<div> <div class="flex items-center relative">
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
<div class=" flex-1 self-center">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter language codes')} placeholder={$i18n.t('Enter language codes')}
bind:value={youtubeLanguage} bind:value={youtubeLanguage}
@@ -470,14 +502,14 @@
/> />
</div> </div>
</div> </div>
</div>
<div> <div class=" mb-2.5 flex flex-col w-full justify-between">
<div class=" py-0.5 flex w-full justify-between"> <div class=" mb-1 text-xs font-medium">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div> {$i18n.t('Youtube Proxy URL')}
<div class=" flex-1 self-center"> </div>
<div class="flex items-center relative">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')} placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
bind:value={youtubeProxyUrl} bind:value={youtubeProxyUrl}
@@ -85,8 +85,9 @@
return true; return true;
} else { } else {
let name = user.name.toLowerCase(); let name = user.name.toLowerCase();
let email = user.email.toLowerCase();
const query = search.toLowerCase(); const query = search.toLowerCase();
return name.includes(query); return name.includes(query) || email.includes(query);
} }
}) })
.sort((a, b) => { .sort((a, b) => {
@@ -157,22 +157,6 @@
} }
files = [...files, fileItem]; files = [...files, fileItem];
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
fileItem.name = file.name;
fileItem.size = file.size;
}
}
try { try {
// During the file upload, file content is automatically extracted. // During the file upload, file content is automatically extracted.
+21 -4
View File
@@ -187,15 +187,20 @@
setToolIds(); setToolIds();
} }
$: if (atSelectedModel || selectedModels) {
setToolIds();
}
const setToolIds = async () => { const setToolIds = async () => {
if (!$tools) { if (!$tools) {
tools.set(await getTools(localStorage.token)); tools.set(await getTools(localStorage.token));
} }
if (selectedModels.length !== 1) { if (selectedModels.length !== 1 && !atSelectedModel) {
return; return;
} }
const model = $models.find((m) => m.id === selectedModels[0]);
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
if (model) { if (model) {
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) => selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
$tools.find((t) => t.id === id) $tools.find((t) => t.id === id)
@@ -836,6 +841,7 @@
content: m.content, content: m.content,
info: m.info ? m.info : undefined, info: m.info ? m.info : undefined,
timestamp: m.timestamp, timestamp: m.timestamp,
...(m.usage ? { usage: m.usage } : {}),
...(m.sources ? { sources: m.sources } : {}) ...(m.sources ? { sources: m.sources } : {})
})), })),
model_item: $models.find((m) => m.id === modelId), model_item: $models.find((m) => m.id === modelId),
@@ -1273,7 +1279,9 @@
const chatInputElement = document.getElementById('chat-input'); const chatInputElement = document.getElementById('chat-input');
if (chatInputElement) { if (chatInputElement) {
await tick();
chatInputElement.style.height = ''; chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 320) + 'px';
} }
const _files = JSON.parse(JSON.stringify(files)); const _files = JSON.parse(JSON.stringify(files));
@@ -1488,7 +1496,10 @@
params?.system ?? $settings?.system ?? '', params?.system ?? $settings?.system ?? '',
$user.name, $user.name,
$settings?.userLocation $settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token) ? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
console.error(err);
return undefined;
})
: undefined : undefined
)}${ )}${
(responseMessage?.userContext ?? null) (responseMessage?.userContext ?? null)
@@ -1573,7 +1584,12 @@
variables: { variables: {
...getPromptVariables( ...getPromptVariables(
$user.name, $user.name,
$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined $settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
console.error(err);
return undefined;
})
: undefined
) )
}, },
model_item: $models.find((m) => m.id === model.id), model_item: $models.find((m) => m.id === model.id),
@@ -1965,6 +1981,7 @@
bind:autoScroll bind:autoScroll
bind:prompt bind:prompt
{selectedModels} {selectedModels}
{atSelectedModel}
{sendPrompt} {sendPrompt}
{showMessage} {showMessage}
{submitMessage} {submitMessage}
@@ -16,6 +16,7 @@
export let modelIds = []; export let modelIds = [];
export let models = []; export let models = [];
export let atSelectedModel;
export let submitPrompt; export let submitPrompt;
@@ -126,7 +127,8 @@
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}> <div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions <Suggestions
className="grid grid-cols-2" className="grid grid-cols-2"
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ?? suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ?? $config?.default_prompt_suggestions ??
[]} []}
on:select={(e) => { on:select={(e) => {
+37 -26
View File
@@ -2,6 +2,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker'; import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte'; import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -173,22 +174,6 @@
} }
files = [...files, fileItem]; files = [...files, fileItem];
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
fileItem.name = file.name;
fileItem.size = file.size;
}
}
try { try {
// During the file upload, file content is automatically extracted. // During the file upload, file content is automatically extracted.
@@ -430,7 +415,7 @@
</div> </div>
{/if} {/if}
{#if webSearchEnabled || ($settings?.webSearch ?? false) === 'always'} {#if webSearchEnabled || ($config?.features?.enable_web_search && ($settings?.webSearch ?? false)) === 'always'}
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500"> <div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
<div class="pl-1"> <div class="pl-1">
@@ -827,7 +812,11 @@
} }
// Submit the prompt when Enter key is pressed // Submit the prompt when Enter key is pressed
if (prompt !== '' && e.keyCode === 13 && !e.shiftKey) { if (
(prompt !== '' || files.length > 0) &&
e.keyCode === 13 &&
!e.shiftKey
) {
dispatch('submit', prompt); dispatch('submit', prompt);
} }
} }
@@ -906,7 +895,11 @@
} }
// Submit the prompt when Enter key is pressed // Submit the prompt when Enter key is pressed
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) { if (
(prompt !== '' || files.length > 0) &&
e.key === 'Enter' &&
!e.shiftKey
) {
dispatch('submit', prompt); dispatch('submit', prompt);
} }
} }
@@ -1108,6 +1101,21 @@
); );
} }
}} }}
uploadOneDriveHandler={async () => {
try {
const fileData = await pickAndDownloadFile();
if (fileData) {
const file = new File([fileData.blob], fileData.name, {
type: fileData.blob.type || 'application/octet-stream'
});
await uploadFileHandler(file);
} else {
console.log('No file was selected from OneDrive');
}
} catch (error) {
console.error('OneDrive Error:', error);
}
}}
onClose={async () => { onClose={async () => {
await tick(); await tick();
@@ -1285,14 +1293,17 @@
stream = null; stream = null;
if (!$TTSWorker) { if ($settings.audio?.tts?.engine === 'browser-kokoro') {
await TTSWorker.set( // If the user has not initialized the TTS worker, initialize it
new KokoroWorker({ if (!$TTSWorker) {
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32' await TTSWorker.set(
}) new KokoroWorker({
); dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init(); await $TTSWorker.init();
}
} }
showCallOverlay.set(true); showCallOverlay.set(true);
@@ -74,7 +74,13 @@
} }
if (command.content.includes('{{USER_LOCATION}}')) { if (command.content.includes('{{USER_LOCATION}}')) {
const location = await getUserPosition(); let location;
try {
location = await getUserPosition();
} catch (error) {
toast.error($i18n.t('Location access not allowed'));
location = 'LOCATION_UNKNOWN';
}
text = text.replaceAll('{{USER_LOCATION}}', String(location)); text = text.replaceAll('{{USER_LOCATION}}', String(location));
} }
@@ -5,6 +5,7 @@
import { config, user, tools as _tools, mobile } from '$lib/stores'; import { config, user, tools as _tools, mobile } from '$lib/stores';
import { createPicker } from '$lib/utils/google-drive-picker'; import { createPicker } from '$lib/utils/google-drive-picker';
import { getTools } from '$lib/apis/tools'; import { getTools } from '$lib/apis/tools';
import Dropdown from '$lib/components/common/Dropdown.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte';
@@ -24,6 +25,7 @@
export let inputFilesHandler: Function; export let inputFilesHandler: Function;
export let uploadGoogleDriveHandler: Function; export let uploadGoogleDriveHandler: Function;
export let uploadOneDriveHandler: Function;
export let selectedToolIds: string[] = []; export let selectedToolIds: string[] = [];
@@ -225,6 +227,97 @@
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div> <div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.Item> </DropdownMenu.Item>
{/if} {/if}
{#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content> </DropdownMenu.Content>
</div> </div>
</Dropdown> </Dropdown>
+2
View File
@@ -32,6 +32,7 @@
export let prompt; export let prompt;
export let history = {}; export let history = {};
export let selectedModels; export let selectedModels;
export let atSelectedModel;
let messages = []; let messages = [];
@@ -349,6 +350,7 @@
{#if Object.keys(history?.messages ?? {}).length == 0} {#if Object.keys(history?.messages ?? {}).length == 0}
<ChatPlaceholder <ChatPlaceholder
modelIds={selectedModels} modelIds={selectedModels}
{atSelectedModel}
submitPrompt={async (p) => { submitPrompt={async (p) => {
let text = p; let text = p;
@@ -7,6 +7,7 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let id = '';
export let sources = []; export let sources = [];
let citations = []; let citations = [];
@@ -42,6 +43,7 @@
} }
$: { $: {
console.log('sources', sources);
citations = sources.reduce((acc, source) => { citations = sources.reduce((acc, source) => {
if (Object.keys(source).length === 0) { if (Object.keys(source).length === 0) {
return acc; return acc;
@@ -52,7 +54,7 @@
const distance = source.distances?.[index]; const distance = source.distances?.[index];
// Within the same citation there could be multiple documents // Within the same citation there could be multiple documents
const id = metadata?.source ?? 'N/A'; const id = metadata?.source ?? source?.source?.id ?? 'N/A';
let _source = source?.source; let _source = source?.source;
if (metadata?.name) { if (metadata?.name) {
@@ -100,7 +102,7 @@
<div class="flex text-xs font-medium flex-wrap"> <div class="flex text-xs font-medium flex-wrap">
{#each citations as citation, idx} {#each citations as citation, idx}
<button <button
id={`source-${idx}`} id={`source-${id}-${idx}`}
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96" class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
on:click={() => { on:click={() => {
showCitationModal = true; showCitationModal = true;
@@ -179,7 +181,7 @@
<div class="flex text-xs font-medium flex-wrap"> <div class="flex text-xs font-medium flex-wrap">
{#each citations as citation, idx} {#each citations as citation, idx}
<button <button
id={`source-${idx}`} id={`source-${id}-${idx}`}
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96" class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
on:click={() => { on:click={() => {
showCitationModal = true; showCitationModal = true;
+120 -30
View File
@@ -1,18 +1,9 @@
<script lang="ts"> <script lang="ts">
import hljs from 'highlight.js';
import { loadPyodide } from 'pyodide';
import mermaid from 'mermaid'; import mermaid from 'mermaid';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { import { getContext, onMount, tick, onDestroy } from 'svelte';
getContext,
getAllContexts,
onMount,
tick,
createEventDispatcher,
onDestroy
} from 'svelte';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css'; import 'highlight.js/styles/github-dark.min.css';
@@ -25,10 +16,12 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let id = ''; export let id = '';
export let onSave = (e) => {};
export let onCode = (e) => {};
export let save = false; export let save = false;
export let run = true; export let run = true;
@@ -71,7 +64,7 @@
saved = true; saved = true;
code = _code; code = _code;
dispatch('save', code); onSave(code);
setTimeout(() => { setTimeout(() => {
saved = false; saved = false;
@@ -123,6 +116,12 @@
}; };
const executePython = async (code) => { const executePython = async (code) => {
result = null;
stdout = null;
stderr = null;
executing = true;
if ($config?.code?.engine === 'jupyter') { if ($config?.code?.engine === 'jupyter') {
const output = await executeCode(localStorage.token, code).catch((error) => { const output = await executeCode(localStorage.token, code).catch((error) => {
toast.error(`${error}`); toast.error(`${error}`);
@@ -130,22 +129,74 @@
}); });
if (output) { if (output) {
stdout = output.stdout; if (output['stdout']) {
stderr = output.stderr; stdout = output['stdout'];
result = output.result; const stdoutLines = stdout.split('\n');
for (const [idx, line] of stdoutLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
if (stdout.startsWith(`${line}\n`)) {
stdout = stdout.replace(`${line}\n`, ``);
} else if (stdout.startsWith(`${line}`)) {
stdout = stdout.replace(`${line}`, ``);
}
}
}
}
if (output['result']) {
result = output['result'];
const resultLines = result.split('\n');
for (const [idx, line] of resultLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
if (result.startsWith(`${line}\n`)) {
result = result.replace(`${line}\n`, ``);
} else if (result.startsWith(`${line}`)) {
result = result.replace(`${line}`, ``);
}
}
}
}
output['stderr'] && (stderr = output['stderr']);
} }
executing = false;
} else { } else {
executePythonAsWorker(code); executePythonAsWorker(code);
} }
}; };
const executePythonAsWorker = async (code) => { const executePythonAsWorker = async (code) => {
result = null;
stdout = null;
stderr = null;
executing = true;
let packages = [ let packages = [
code.includes('requests') ? 'requests' : null, code.includes('requests') ? 'requests' : null,
code.includes('bs4') ? 'beautifulsoup4' : null, code.includes('bs4') ? 'beautifulsoup4' : null,
@@ -205,7 +256,40 @@
]; ];
} }
stdout = stdout.replace(`${line}\n`, ``); if (stdout.startsWith(`${line}\n`)) {
stdout = stdout.replace(`${line}\n`, ``);
} else if (stdout.startsWith(`${line}`)) {
stdout = stdout.replace(`${line}`, ``);
}
}
}
}
if (data['result']) {
result = data['result'];
const resultLines = result.split('\n');
for (const [idx, line] of resultLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
if (result.startsWith(`${line}\n`)) {
result = result.replace(`${line}\n`, ``);
} else if (result.startsWith(`${line}`)) {
result = result.replace(`${line}`, ``);
}
} }
} }
} }
@@ -253,7 +337,7 @@
render(); render();
} }
$: dispatch('code', { lang, code }); $: onCode({ lang, code });
$: if (attributes) { $: if (attributes) {
onAttributesUpdate(); onAttributesUpdate();
@@ -289,7 +373,7 @@
console.log('codeblock', lang, code); console.log('codeblock', lang, code);
if (lang) { if (lang) {
dispatch('code', { lang, code }); onCode({ lang, code });
} }
if (document.documentElement.classList.contains('dark')) { if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({ mermaid.initialize({
@@ -377,11 +461,11 @@
value={code} value={code}
{id} {id}
{lang} {lang}
on:save={() => { onSave={() => {
saveCode(); saveCode();
}} }}
on:change={(e) => { onChange={(value) => {
_code = e.detail.value; _code = value;
}} }}
/> />
</div> </div>
@@ -391,7 +475,7 @@
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden" class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
/> />
{#if executing || stdout || stderr || result} {#if executing || stdout || stderr || result || files}
<div <div
class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2" class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
> >
@@ -404,7 +488,13 @@
{#if stdout || stderr} {#if stdout || stderr}
<div class=" "> <div class=" ">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div> <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr}</div> <div
class="text-sm {stdout?.split('\n')?.length > 100
? `max-h-96`
: ''} overflow-y-auto"
>
{stdout || stderr}
</div>
</div> </div>
{/if} {/if}
{#if result || files} {#if result || files}
@@ -417,7 +507,7 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each files as file} {#each files as file}
{#if file.type.startsWith('image')} {#if file.type.startsWith('image')}
<img src={file.data} alt="Output" /> <img src={file.data} alt="Output" class=" w-full max-w-[36rem]" />
{/if} {/if}
{/each} {/each}
</div> </div>
@@ -120,6 +120,11 @@
sourceIds={(sources ?? []).reduce((acc, s) => { sourceIds={(sources ?? []).reduce((acc, s) => {
let ids = []; let ids = [];
s.document.forEach((document, index) => { s.document.forEach((document, index) => {
if (model?.info?.meta?.capabilities?.citations == false) {
ids.push('N/A');
return ids;
}
const metadata = s.metadata?.[index]; const metadata = s.metadata?.[index];
const id = metadata?.source ?? 'N/A'; const id = metadata?.source ?? 'N/A';
@@ -29,7 +29,7 @@
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)} {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
{@html `${token.text}`} {@html `${token.text}`}
{:else if token.text.includes(`<source_id`)} {:else if token.text.includes(`<source_id`)}
<Source {token} onClick={onSourceClick} /> <Source {id} {token} onClick={onSourceClick} />
{:else} {:else}
{token.text} {token.text}
{/if} {/if}
@@ -76,7 +76,7 @@
{#if token.type === 'hr'} {#if token.type === 'hr'}
<hr class=" border-gray-100 dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
{:else if token.type === 'heading'} {:else if token.type === 'heading'}
<svelte:element this={headerComponent(token.depth)}> <svelte:element this={headerComponent(token.depth)} dir="auto">
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} /> <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
</svelte:element> </svelte:element>
{:else if token.type === 'code'} {:else if token.type === 'code'}
@@ -88,14 +88,14 @@
code={token?.text ?? ''} code={token?.text ?? ''}
{attributes} {attributes}
{save} {save}
on:code={(e) => { onCode={(value) => {
dispatch('code', e.detail); dispatch('code', value);
}} }}
on:save={(e) => { onSave={(e) => {
dispatch('update', { dispatch('update', {
raw: token.raw, raw: token.raw,
oldContent: token.text, oldContent: token.text,
newContent: e.detail newContent: value
}); });
}} }}
/> />
@@ -176,7 +176,7 @@
{#if token.ordered} {#if token.ordered}
<ol start={token.start || 1}> <ol start={token.start || 1}>
{#each token.items as item, itemIdx} {#each token.items as item, itemIdx}
<li dir="auto"> <li dir="auto" class="text-start">
{#if item?.task} {#if item?.task}
<input <input
class=" translate-y-[1px] -translate-x-1" class=" translate-y-[1px] -translate-x-1"
@@ -208,7 +208,7 @@
{:else} {:else}
<ul> <ul>
{#each token.items as item, itemIdx} {#each token.items as item, itemIdx}
<li dir="auto"> <li dir="auto" class="text-start">
{#if item?.task} {#if item?.task}
<input <input
class=" translate-y-[1px] -translate-x-1" class=" translate-y-[1px] -translate-x-1"
@@ -239,7 +239,12 @@
</ul> </ul>
{/if} {/if}
{:else if token.type === 'details'} {:else if token.type === 'details'}
<Collapsible title={token.summary} attributes={token?.attributes} className="w-full space-y-1"> <Collapsible
title={token.summary}
attributes={token?.attributes}
className="w-full space-y-1"
dir="auto"
>
<div class=" mb-1.5" slot="content"> <div class=" mb-1.5" slot="content">
<svelte:self <svelte:self
id={`${id}-${tokenIdx}-d`} id={`${id}-${tokenIdx}-d`}
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
export let id;
export let token; export let token;
export let onClick: Function = () => {}; export let onClick: Function = () => {};
let attributes: Record<string, string> = {}; let attributes: Record<string, string | undefined> = {};
function extractAttributes(input: string): Record<string, string> { function extractAttributes(input: string): Record<string, string> {
const regex = /(\w+)="([^"]*)"/g; const regex = /(\w+)="([^"]*)"/g;
@@ -35,13 +36,15 @@
$: attributes = extractAttributes(token.text); $: attributes = extractAttributes(token.text);
</script> </script>
<button {#if attributes.title !== 'N/A'}
class="text-xs font-medium w-fit translate-y-[2px] px-2 py-0.5 dark:bg-white/5 dark:text-white/60 dark:hover:text-white bg-gray-50 text-black/60 hover:text-black transition rounded-lg" <button
on:click={() => { class="text-xs font-medium w-fit translate-y-[2px] px-2 py-0.5 dark:bg-white/5 dark:text-white/60 dark:hover:text-white bg-gray-50 text-black/60 hover:text-black transition rounded-lg"
onClick(attributes.data); on:click={() => {
}} onClick(id, attributes.data);
> }}
<span class="line-clamp-1"> >
{formattedTitle(attributes.title)} <span class="line-clamp-1">
</span> {attributes.title ? formattedTitle(attributes.title) : ''}
</button> </span>
</button>
{/if}
@@ -129,6 +129,7 @@
export let isLastMessage = true; export let isLastMessage = true;
export let readOnly = false; export let readOnly = false;
let buttonsContainerElement: HTMLDivElement;
let showDeleteConfirm = false; let showDeleteConfirm = false;
let model = null; let model = null;
@@ -518,6 +519,18 @@
// console.log('ResponseMessage mounted'); // console.log('ResponseMessage mounted');
await tick(); await tick();
if (buttonsContainerElement) {
console.log(buttonsContainerElement);
buttonsContainerElement.addEventListener('wheel', function (event) {
// console.log(event.deltaY);
event.preventDefault();
if (event.deltaY !== 0) {
// Adjust horizontal scroll position based on vertical scroll
buttonsContainerElement.scrollLeft += event.deltaY;
}
});
}
}); });
</script> </script>
@@ -732,9 +745,9 @@
onTaskClick={async (e) => { onTaskClick={async (e) => {
console.log(e); console.log(e);
}} }}
onSourceClick={async (e) => { onSourceClick={async (id, idx) => {
console.log(e); console.log(id, idx);
let sourceButton = document.getElementById(`source-${e}`); let sourceButton = document.getElementById(`source-${message.id}-${idx}`);
const sourcesCollapsible = document.getElementById(`collapsible-sources`); const sourcesCollapsible = document.getElementById(`collapsible-sources`);
if (sourceButton) { if (sourceButton) {
@@ -753,7 +766,7 @@
}); });
// Try clicking the source button again // Try clicking the source button again
sourceButton = document.getElementById(`source-${e}`); sourceButton = document.getElementById(`source-${message.id}-${idx}`);
sourceButton && sourceButton.click(); sourceButton && sourceButton.click();
} }
}} }}
@@ -790,7 +803,7 @@
{/if} {/if}
{#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)} {#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)}
<Citations sources={message?.sources ?? message?.citations} /> <Citations id={message?.id} sources={message?.sources ?? message?.citations} />
{/if} {/if}
{#if message.code_executions} {#if message.code_executions}
@@ -802,10 +815,11 @@
</div> </div>
{#if !edit} {#if !edit}
{#if message.done || siblings.length > 1} <div
<div bind:this={buttonsContainerElement}
class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500 mt-0.5" class="flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500 mt-0.5"
> >
{#if message.done || siblings.length > 1}
{#if siblings.length > 1} {#if siblings.length > 1}
<div class="flex self-center min-w-fit" dir="ltr"> <div class="flex self-center min-w-fit" dir="ltr">
<button <button
@@ -1313,8 +1327,8 @@
{/if} {/if}
{/if} {/if}
{/if} {/if}
</div> {/if}
{/if} </div>
{#if message.done && showRateComment} {#if message.done && showRateComment}
<RateComment <RateComment
+2 -1
View File
@@ -213,7 +213,8 @@
<div class="mx-auto max-w-2xl font-primary" in:fade={{ duration: 200, delay: 200 }}> <div class="mx-auto max-w-2xl font-primary" in:fade={{ duration: 200, delay: 200 }}>
<div class="mx-5"> <div class="mx-5">
<Suggestions <Suggestions
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ?? suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ?? $config?.default_prompt_suggestions ??
[]} []}
inputValue={prompt} inputValue={prompt}
@@ -12,6 +12,7 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import EditMemoryModal from './EditMemoryModal.svelte'; import EditMemoryModal from './EditMemoryModal.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@@ -26,6 +27,21 @@
let selectedMemory = null; let selectedMemory = null;
let showClearConfirmDialog = false;
let onClearConfirmed = async () => {
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res && memories.length > 0) {
toast.success($i18n.t('Memory cleared successfully'));
memories = [];
}
showClearConfirmDialog = false;
};
$: if (show && memories.length === 0 && loading) { $: if (show && memories.length === 0 && loading) {
(async () => { (async () => {
memories = await getMemories(localStorage.token); memories = await getMemories(localStorage.token);
@@ -175,15 +191,11 @@
> >
<button <button
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl" class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
on:click={async () => { on:click={() => {
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => { if (memories.length > 0) {
toast.error(`${error}`); showClearConfirmDialog = true;
return null; } else {
}); toast.error($i18n.t('No memories to clear'));
if (res) {
toast.success($i18n.t('Memory cleared successfully'));
memories = [];
} }
}}>{$i18n.t('Clear memory')}</button }}>{$i18n.t('Clear memory')}</button
> >
@@ -192,6 +204,16 @@
</div> </div>
</Modal> </Modal>
<ConfirmDialog
title={$i18n.t('Clear Memory')}
message={$i18n.t('Are you sure you want to clear all memories? This action cannot be undone.')}
show={showClearConfirmDialog}
on:confirm={onClearConfirmed}
on:cancel={() => {
showClearConfirmDialog = false;
}}
/>
<AddMemoryModal <AddMemoryModal
bind:show={showAddMemoryModal} bind:show={showAddMemoryModal}
on:save={async () => { on:save={async () => {
+40 -2
View File
@@ -122,6 +122,30 @@
</div> </div>
</div> </div>
</div> </div>
<div class="w-full flex justify-between items-center">
<div class=" text-sm">{$i18n.t('Generate prompt pair')}</div>
<div class="flex space-x-1 text-xs">
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Ctrl/⌘
</div>
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Shift
</div>
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Enter
</div>
</div>
</div>
</div> </div>
<div class="flex flex-col space-y-3 w-full self-start"> <div class="flex flex-col space-y-3 w-full self-start">
@@ -219,7 +243,7 @@
<div class="flex flex-col space-y-3 w-full self-start"> <div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm"> <div class=" text-sm">
{$i18n.t('Attach file')} {$i18n.t('Attach file from knowledge')}
</div> </div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
@@ -247,7 +271,7 @@
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm"> <div class=" text-sm">
{$i18n.t('Select model')} {$i18n.t('Talk to model')}
</div> </div>
<div class="flex space-x-1 text-xs"> <div class="flex space-x-1 text-xs">
@@ -258,6 +282,20 @@
</div> </div>
</div> </div>
</div> </div>
<div class="w-full flex justify-between items-center">
<div class=" text-sm">
{$i18n.t('Accept autocomplete generation / Jump to prompt variable')}
</div>
<div class="flex space-x-1 text-xs">
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
TAB
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
+12 -3
View File
@@ -21,6 +21,10 @@
export let boilerplate = ''; export let boilerplate = '';
export let value = ''; export let value = '';
export let onSave = () => {};
export let onChange = () => {};
let _value = ''; let _value = '';
$: if (value) { $: if (value) {
@@ -43,6 +47,10 @@
let codeEditor; let codeEditor;
export const focus = () => {
codeEditor.focus();
};
let isDarkMode = false; let isDarkMode = false;
let editorTheme = new Compartment(); let editorTheme = new Compartment();
let editorLanguage = new Compartment(); let editorLanguage = new Compartment();
@@ -75,7 +83,7 @@
}); });
_value = formattedCode; _value = formattedCode;
dispatch('change', { value: _value }); onChange(_value);
await tick(); await tick();
toast.success($i18n.t('Code formatted successfully')); toast.success($i18n.t('Code formatted successfully'));
@@ -94,7 +102,7 @@
EditorView.updateListener.of((e) => { EditorView.updateListener.of((e) => {
if (e.docChanged) { if (e.docChanged) {
_value = e.state.doc.toString(); _value = e.state.doc.toString();
dispatch('change', { value: _value }); onChange(_value);
} }
}), }),
editorTheme.of([]), editorTheme.of([]),
@@ -170,7 +178,8 @@
const keydownHandler = async (e) => { const keydownHandler = async (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') { if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault(); e.preventDefault();
dispatch('save');
onSave();
} }
// Format code when Ctrl + Shift + F is pressed // Format code when Ctrl + Shift + F is pressed
+26 -7
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
import { formatFileSize, getLineCount } from '$lib/utils'; import { formatFileSize, getLineCount } from '$lib/utils';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@@ -12,14 +13,15 @@
export let item; export let item;
export let show = false; export let show = false;
export let edit = false; export let edit = false;
let enableFullContent = false; let enableFullContent = false;
$: isPDF =
item?.meta?.content_type === 'application/pdf' ||
(item?.name && item?.name.toLowerCase().endsWith('.pdf'));
onMount(() => { onMount(() => {
console.log(item); console.log(item);
if (item?.context === 'full') { if (item?.context === 'full') {
enableFullContent = true; enableFullContent = true;
} }
@@ -33,9 +35,16 @@
<div> <div>
<div class=" font-medium text-lg dark:text-gray-100"> <div class=" font-medium text-lg dark:text-gray-100">
<a <a
href={item.url ? (item.type === 'file' ? `${item.url}/content` : `${item.url}`) : '#'} href="#"
target="_blank"
class="hover:underline line-clamp-1" class="hover:underline line-clamp-1"
on:click|preventDefault={() => {
if (!isPDF && item.url) {
window.open(
item.type === 'file' ? `${item.url}/content` : `${item.url}`,
'_blank'
);
}
}}
> >
{item?.name ?? 'File'} {item?.name ?? 'File'}
</a> </a>
@@ -78,7 +87,7 @@
<div> <div>
<Tooltip <Tooltip
content={enableFullContent content={enableFullContent
? 'Inject the entire document as context for comprehensive processing, this is recommended for complex queries.' ? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'} : 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
> >
<div class="flex items-center gap-1.5 text-xs"> <div class="flex items-center gap-1.5 text-xs">
@@ -101,8 +110,18 @@
</div> </div>
</div> </div>
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap"> <div class="max-h-[75vh] overflow-auto">
{item?.file?.data?.content ?? 'No content'} {#if isPDF}
<iframe
title={item?.name}
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
class="w-full h-[70vh] border-0 rounded-lg mt-4"
/>
{:else}
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
{item?.file?.data?.content ?? 'No content'}
</div>
{/if}
</div> </div>
</div> </div>
</Modal> </Modal>
+1 -2
View File
@@ -6,7 +6,6 @@
export let show = true; export let show = true;
export let size = 'md'; export let size = 'md';
export let containerClassName = 'p-3'; export let containerClassName = 'p-3';
export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl'; export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl';
@@ -74,7 +73,7 @@
}} }}
> >
<div <div
class=" m-auto max-w-full {sizeToWidth(size)} {size !== 'full' class="m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
? 'mx-2' ? 'mx-2'
: ''} shadow-3xl min-h-fit scrollbar-hidden {className}" : ''} shadow-3xl min-h-fit scrollbar-hidden {className}"
in:flyAndScale in:flyAndScale
@@ -133,20 +133,6 @@
knowledge.files = [...(knowledge.files ?? []), fileItem]; knowledge.files = [...(knowledge.files ?? []), fileItem];
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
try { try {
const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => { const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
toast.error(`${e}`); toast.error(`${e}`);
@@ -180,7 +180,6 @@
} }
if (model) { if (model) {
console.log(model);
name = model.name; name = model.name;
await tick(); await tick();
@@ -2,6 +2,7 @@
import { DropdownMenu } from 'bits-ui'; import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { config } from '$lib/stores';
import Dropdown from '$lib/components/common/Dropdown.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
@@ -44,15 +45,17 @@
align="start" align="start"
transition={flyAndScale} transition={flyAndScale}
> >
<DropdownMenu.Item {#if $config.features.enable_community_sharing}
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" <DropdownMenu.Item
on:click={() => { class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
shareHandler(); on:click={() => {
}} shareHandler();
> }}
<Share /> >
<div class="flex items-center">{$i18n.t('Share')}</div> <Share />
</DropdownMenu.Item> <div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
@@ -12,6 +12,7 @@
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
import { config } from '$lib/stores';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@@ -69,15 +70,17 @@
<div class="flex items-center">{$i18n.t('Edit')}</div> <div class="flex items-center">{$i18n.t('Edit')}</div>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item {#if $config.features.enable_community_sharing}
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" <DropdownMenu.Item
on:click={() => { class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
shareHandler(); on:click={() => {
}} shareHandler();
> }}
<Share /> >
<div class="flex items-center">{$i18n.t('Share')}</div> <Share />
</DropdownMenu.Item> <div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
@@ -1,5 +1,5 @@
<script> <script>
import { getContext, createEventDispatcher, onMount, tick } from 'svelte'; import { getContext, onMount, tick } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@@ -12,8 +12,6 @@
import LockClosed from '$lib/components/icons/LockClosed.svelte'; import LockClosed from '$lib/components/icons/LockClosed.svelte';
import AccessControlModal from '../common/AccessControlModal.svelte'; import AccessControlModal from '../common/AccessControlModal.svelte';
const dispatch = createEventDispatcher();
let formElement = null; let formElement = null;
let loading = false; let loading = false;
@@ -23,6 +21,8 @@
export let edit = false; export let edit = false;
export let clone = false; export let clone = false;
export let onSave = () => {};
export let id = ''; export let id = '';
export let name = ''; export let name = '';
export let meta = { export let meta = {
@@ -150,7 +150,7 @@ class Tools:
const saveHandler = async () => { const saveHandler = async () => {
loading = true; loading = true;
dispatch('save', { onSave({
id, id,
name, name,
meta, meta,
@@ -284,10 +284,10 @@ class Tools:
value={content} value={content}
{boilerplate} {boilerplate}
lang="python" lang="python"
on:change={(e) => { onChange={(e) => {
_content = e.detail.value; _content = e;
}} }}
on:save={() => { onSave={() => {
if (formElement) { if (formElement) {
formElement.requestSubmit(); formElement.requestSubmit();
} }
+29 -13
View File
@@ -13,6 +13,7 @@
"A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب", "A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب",
"a user": "مستخدم", "a user": "مستخدم",
"About": "عن", "About": "عن",
"Accept autocomplete generation / Jump to prompt variable": "",
"Access": "", "Access": "",
"Access Control": "", "Access Control": "",
"Accessible to all users": "", "Accessible to all users": "",
@@ -85,6 +86,7 @@
"Archive All Chats": "أرشفة جميع الدردشات", "Archive All Chats": "أرشفة جميع الدردشات",
"Archived Chats": "الأرشيف المحادثات", "Archived Chats": "الأرشيف المحادثات",
"archived-chat-export": "", "archived-chat-export": "",
"Are you sure you want to clear all memories? This action cannot be undone.": "",
"Are you sure you want to delete this channel?": "", "Are you sure you want to delete this channel?": "",
"Are you sure you want to delete this message?": "", "Are you sure you want to delete this message?": "",
"Are you sure you want to unarchive all archived chats?": "", "Are you sure you want to unarchive all archived chats?": "",
@@ -93,7 +95,7 @@
"Artifacts": "", "Artifacts": "",
"Ask a question": "", "Ask a question": "",
"Assistant": "", "Assistant": "",
"Attach file": "أرفق ملف", "Attach file from knowledge": "",
"Attention to detail": "انتبه للتفاصيل", "Attention to detail": "انتبه للتفاصيل",
"Attribute for Mail": "", "Attribute for Mail": "",
"Attribute for Username": "", "Attribute for Username": "",
@@ -127,6 +129,7 @@
"Bocha Search API Key": "", "Bocha Search API Key": "",
"Brave Search API Key": "مفتاح واجهة برمجة تطبيقات البحث الشجاع", "Brave Search API Key": "مفتاح واجهة برمجة تطبيقات البحث الشجاع",
"By {{name}}": "", "By {{name}}": "",
"Bypass Embedding and Retrieval": "",
"Bypass SSL verification for Websites": "تجاوز التحقق من SSL للموقع", "Bypass SSL verification for Websites": "تجاوز التحقق من SSL للموقع",
"Calendar": "", "Calendar": "",
"Call": "", "Call": "",
@@ -156,11 +159,11 @@
"Checking for updates...": "البحث عن تحديثات", "Checking for updates...": "البحث عن تحديثات",
"Choose a model before saving...": "أختار موديل قبل الحفظ", "Choose a model before saving...": "أختار موديل قبل الحفظ",
"Chunk Overlap": "Chunk تداخل", "Chunk Overlap": "Chunk تداخل",
"Chunk Params": "Chunk المتغيرات",
"Chunk Size": "Chunk حجم", "Chunk Size": "Chunk حجم",
"Ciphers": "", "Ciphers": "",
"Citation": "اقتباس", "Citation": "اقتباس",
"Clear memory": "", "Clear memory": "",
"Clear Memory": "",
"click here": "", "click here": "",
"Click here for filter guides.": "", "Click here for filter guides.": "",
"Click here for help.": "أضغط هنا للمساعدة", "Click here for help.": "أضغط هنا للمساعدة",
@@ -182,6 +185,7 @@
"Code execution": "", "Code execution": "",
"Code Execution": "", "Code Execution": "",
"Code Execution Engine": "", "Code Execution Engine": "",
"Code Execution Timeout": "",
"Code formatted successfully": "", "Code formatted successfully": "",
"Code Interpreter": "", "Code Interpreter": "",
"Code Interpreter Engine": "", "Code Interpreter Engine": "",
@@ -207,7 +211,7 @@
"Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)": "", "Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)": "",
"Contact Admin for WebUI Access": "", "Contact Admin for WebUI Access": "",
"Content": "الاتصال", "Content": "الاتصال",
"Content Extraction": "", "Content Extraction Engine": "",
"Context Length": "طول السياق", "Context Length": "طول السياق",
"Continue Response": "متابعة الرد", "Continue Response": "متابعة الرد",
"Continue with {{provider}}": "", "Continue with {{provider}}": "",
@@ -244,6 +248,7 @@
"Current Model": "الموديل المختار", "Current Model": "الموديل المختار",
"Current Password": "كلمة السر الحالية", "Current Password": "كلمة السر الحالية",
"Custom": "مخصص", "Custom": "مخصص",
"Danger Zone": "",
"Dark": "مظلم", "Dark": "مظلم",
"Database": "قاعدة البيانات", "Database": "قاعدة البيانات",
"December": "ديسمبر", "December": "ديسمبر",
@@ -304,6 +309,8 @@
"Do not install functions from sources you do not fully trust.": "", "Do not install functions from sources you do not fully trust.": "",
"Do not install tools from sources you do not fully trust.": "", "Do not install tools from sources you do not fully trust.": "",
"Document": "المستند", "Document": "المستند",
"Document Intelligence": "",
"Document Intelligence endpoint and key required.": "",
"Documentation": "", "Documentation": "",
"Documents": "مستندات", "Documents": "مستندات",
"does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.", "does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.",
@@ -321,6 +328,7 @@
"Draw": "", "Draw": "",
"Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة", "Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.", "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.",
"e.g. 60": "",
"e.g. A filter to remove profanity from text": "", "e.g. A filter to remove profanity from text": "",
"e.g. My Filter": "", "e.g. My Filter": "",
"e.g. My Tools": "", "e.g. My Tools": "",
@@ -338,6 +346,7 @@
"ElevenLabs": "", "ElevenLabs": "",
"Email": "البريد", "Email": "البريد",
"Embark on adventures": "", "Embark on adventures": "",
"Embedding": "",
"Embedding Batch Size": "", "Embedding Batch Size": "",
"Embedding Model": "نموذج التضمين", "Embedding Model": "نموذج التضمين",
"Embedding Model Engine": "تضمين محرك النموذج", "Embedding Model Engine": "تضمين محرك النموذج",
@@ -346,15 +355,12 @@
"Enable autocomplete generation for chat messages": "", "Enable autocomplete generation for chat messages": "",
"Enable Code Interpreter": "", "Enable Code Interpreter": "",
"Enable Community Sharing": "تمكين مشاركة المجتمع", "Enable Community Sharing": "تمكين مشاركة المجتمع",
"Enable Google Drive": "",
"Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "", "Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "",
"Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "", "Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "",
"Enable Message Rating": "", "Enable Message Rating": "",
"Enable Mirostat sampling for controlling perplexity. (Default: 0, 0 = Disabled, 1 = Mirostat, 2 = Mirostat 2.0)": "", "Enable Mirostat sampling for controlling perplexity. (Default: 0, 0 = Disabled, 1 = Mirostat, 2 = Mirostat 2.0)": "",
"Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة", "Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة",
"Enable Web Search": "تمكين بحث الويب",
"Enabled": "", "Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.", "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا", "Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل", "Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
@@ -370,6 +376,8 @@
"Enter Chunk Overlap": "أدخل الChunk Overlap", "Enter Chunk Overlap": "أدخل الChunk Overlap",
"Enter Chunk Size": "أدخل Chunk الحجم", "Enter Chunk Size": "أدخل Chunk الحجم",
"Enter description": "", "Enter description": "",
"Enter Document Intelligence Endpoint": "",
"Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "", "Enter domains separated by commas (e.g., example.com,site.org)": "",
"Enter Exa API Key": "", "Enter Exa API Key": "",
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw", "Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
@@ -408,6 +416,7 @@
"Enter Tavily API Key": "", "Enter Tavily API Key": "",
"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "", "Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "",
"Enter Tika Server URL": "", "Enter Tika Server URL": "",
"Enter timeout in seconds": "",
"Enter Top K": "أدخل Top K", "Enter Top K": "أدخل Top K",
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)", "Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)", "Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)",
@@ -500,10 +509,13 @@
"Functions allow arbitrary code execution": "", "Functions allow arbitrary code execution": "",
"Functions allow arbitrary code execution.": "", "Functions allow arbitrary code execution.": "",
"Functions imported successfully": "", "Functions imported successfully": "",
"Gemini": "",
"Gemini API Config": "",
"Gemini API Key is required.": "",
"General": "عام", "General": "عام",
"General Settings": "الاعدادات العامة",
"Generate an image": "", "Generate an image": "",
"Generate Image": "", "Generate Image": "",
"Generate prompt pair": "",
"Generating search query": "إنشاء استعلام بحث", "Generating search query": "إنشاء استعلام بحث",
"Get started": "", "Get started": "",
"Get started with {{WEBUI_NAME}}": "", "Get started with {{WEBUI_NAME}}": "",
@@ -559,6 +571,7 @@
"Input commands": "إدخال الأوامر", "Input commands": "إدخال الأوامر",
"Install from Github URL": "التثبيت من عنوان URL لجيثب", "Install from Github URL": "التثبيت من عنوان URL لجيثب",
"Instant Auto-Send After Voice Transcription": "", "Instant Auto-Send After Voice Transcription": "",
"Integration": "",
"Interface": "واجهه المستخدم", "Interface": "واجهه المستخدم",
"Invalid file format.": "", "Invalid file format.": "",
"Invalid Tag": "تاق غير صالحة", "Invalid Tag": "تاق غير صالحة",
@@ -606,16 +619,17 @@
"Listening...": "", "Listening...": "",
"Llama.cpp": "", "Llama.cpp": "",
"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة", "LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
"Loader": "",
"Loading Kokoro.js...": "", "Loading Kokoro.js...": "",
"Local": "", "Local": "",
"Local Models": "", "Local Models": "",
"Location access not allowed": "",
"Lost": "", "Lost": "",
"LTR": "من جهة اليسار إلى اليمين", "LTR": "من جهة اليسار إلى اليمين",
"Made by Open WebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ", "Made by Open WebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
"Make sure to enclose them with": "تأكد من إرفاقها", "Make sure to enclose them with": "تأكد من إرفاقها",
"Make sure to export a workflow.json file as API format from ComfyUI.": "", "Make sure to export a workflow.json file as API format from ComfyUI.": "",
"Manage": "", "Manage": "",
"Manage Arena Models": "",
"Manage Direct Connections": "", "Manage Direct Connections": "",
"Manage Models": "", "Manage Models": "",
"Manage Ollama": "", "Manage Ollama": "",
@@ -683,6 +697,7 @@
"No HTML, CSS, or JavaScript content found.": "", "No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "", "No inference engine with management support found": "",
"No knowledge found": "", "No knowledge found": "",
"No memories to clear": "",
"No model IDs": "", "No model IDs": "",
"No models found": "", "No models found": "",
"No models selected": "", "No models selected": "",
@@ -712,6 +727,7 @@
"Ollama API settings updated": "", "Ollama API settings updated": "",
"Ollama Version": "Ollama الاصدار", "Ollama Version": "Ollama الاصدار",
"On": "تشغيل", "On": "تشغيل",
"OneDrive": "",
"Only alphanumeric characters and hyphens are allowed": "", "Only alphanumeric characters and hyphens are allowed": "",
"Only alphanumeric characters and hyphens are allowed in the command string.": "يُسمح فقط بالأحرف الأبجدية الرقمية والواصلات في سلسلة الأمر.", "Only alphanumeric characters and hyphens are allowed in the command string.": "يُسمح فقط بالأحرف الأبجدية الرقمية والواصلات في سلسلة الأمر.",
"Only collections can be edited, create a new knowledge base to edit/add documents.": "", "Only collections can be edited, create a new knowledge base to edit/add documents.": "",
@@ -782,11 +798,9 @@
"Prompt updated successfully": "", "Prompt updated successfully": "",
"Prompts": "مطالبات", "Prompts": "مطالبات",
"Prompts Access": "", "Prompts Access": "",
"Proxy URL": "",
"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com \"{{searchValue}}\" أسحب من ", "Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com \"{{searchValue}}\" أسحب من ",
"Pull a model from Ollama.com": "Ollama.com سحب الموديل من ", "Pull a model from Ollama.com": "Ollama.com سحب الموديل من ",
"Query Generation Prompt": "", "Query Generation Prompt": "",
"Query Params": "Query Params",
"RAG Template": "RAG تنمبلت", "RAG Template": "RAG تنمبلت",
"Rating": "", "Rating": "",
"Re-rank models by topic similarity": "", "Re-rank models by topic similarity": "",
@@ -821,6 +835,7 @@
"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
"Response splitting": "", "Response splitting": "",
"Result": "", "Result": "",
"Retrieval": "",
"Retrieval Query Generation": "", "Retrieval Query Generation": "",
"Rich Text Input for Chat": "", "Rich Text Input for Chat": "",
"RK": "", "RK": "",
@@ -874,7 +889,6 @@
"Select an Ollama instance": "", "Select an Ollama instance": "",
"Select Engine": "", "Select Engine": "",
"Select Knowledge": "", "Select Knowledge": "",
"Select model": " أختار موديل",
"Select only one model to call": "", "Select only one model to call": "",
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور", "Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
"Semantic distance to query": "", "Semantic distance to query": "",
@@ -951,6 +965,7 @@
"Tags Generation": "", "Tags Generation": "",
"Tags Generation Prompt": "", "Tags Generation Prompt": "",
"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "", "Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "",
"Talk to model": "",
"Tap to interrupt": "", "Tap to interrupt": "",
"Tasks": "", "Tasks": "",
"Tavily API Key": "", "Tavily API Key": "",
@@ -1035,6 +1050,7 @@
"Top P": "Top P", "Top P": "Top P",
"Transformers": "", "Transformers": "",
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول", "Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
"Trust Proxy Environment": "",
"TTS Model": "", "TTS Model": "",
"TTS Settings": "TTS اعدادات", "TTS Settings": "TTS اعدادات",
"TTS Voice": "", "TTS Voice": "",
@@ -1098,7 +1114,6 @@
"Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.": "", "Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.": "",
"Web": "Web", "Web": "Web",
"Web API": "", "Web API": "",
"Web Loader Settings": "Web تحميل اعدادات",
"Web Search": "بحث الويب", "Web Search": "بحث الويب",
"Web Search Engine": "محرك بحث الويب", "Web Search Engine": "محرك بحث الويب",
"Web Search in Chat": "", "Web Search in Chat": "",
@@ -1140,5 +1155,6 @@
"Your account status is currently pending activation.": "", "Your account status is currently pending activation.": "",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
"Youtube": "Youtube", "Youtube": "Youtube",
"Youtube Loader Settings": "Youtube تحميل اعدادات" "Youtube Language": "",
"Youtube Proxy URL": ""
} }
File diff suppressed because it is too large. Load diff
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more