Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95cadaca72 | ||
|
|
e26c443815 | ||
|
|
046f1fb30a | ||
|
|
63d825692e | ||
|
|
70667d5cc8 | ||
|
|
2ac0eae99e | ||
|
|
1dd442f38b | ||
|
|
94c19f73b0 | ||
|
|
ed0b9be4e3 | ||
|
|
866d02bbae | ||
|
|
f7c5cd486b | ||
|
|
660a44c918 | ||
|
|
b6873153c1 | ||
|
|
24a2e84692 | ||
|
|
f643182eb0 | ||
|
|
38fb9d5fd8 | ||
|
|
15485e7c5d | ||
|
|
8241fa2e12 | ||
|
|
37a7dd6d33 | ||
|
|
85b7ec9387 | ||
|
|
b838a1df99 | ||
|
|
8c4d967ef4 | ||
|
|
ce7cf62a55 | ||
|
|
0a4dbf7cf0 | ||
|
|
fcbdfbd744 | ||
|
|
1ae702b8a6 | ||
|
|
2d379cb35f | ||
|
|
76e90d9f3f | ||
|
|
da8c42ee49 | ||
|
|
ddb30589e3 | ||
|
|
ef734833a5 | ||
|
|
40ddd172e0 | ||
|
|
57010901e6 | ||
|
|
1c2e36f1b7 | ||
|
|
34aeaaf020 | ||
|
|
fa91d83ac3 | ||
|
|
78a8ef8e66 | ||
|
|
46ac6f2b29 | ||
|
|
33d3558ca9 | ||
|
|
c1c92b0c03 | ||
|
|
618307f35b | ||
|
|
c5a09cdd21 | ||
|
|
661711164a | ||
|
|
80efaaffdd | ||
|
|
46f2dc0d11 | ||
|
|
674d6e08fc | ||
|
|
76891f4760 | ||
|
|
20f0b02b38 | ||
|
|
bef7a532cc | ||
|
|
ced508c912 | ||
|
|
c883368842 | ||
|
|
8097b6a3ff | ||
|
|
7cfbf6489a | ||
|
|
11967a780d | ||
|
|
6cf0ceedba | ||
|
|
ec72dcba75 | ||
|
|
54d7bf8f28 | ||
|
|
3be5e3129b | ||
|
|
2298bfbd7e | ||
|
|
0e5d5ecb81 | ||
|
|
b52a7e3543 | ||
|
|
df1238229c | ||
|
|
eafefcf838 | ||
|
|
9f27d7710b | ||
|
|
7c68a424f8 | ||
|
|
5d3249848d | ||
|
|
7e2b2ec761 | ||
|
|
b964b175d4 | ||
|
|
46c4da4864 | ||
|
|
126045318f | ||
|
|
205013da2f | ||
|
|
abf36c0645 | ||
|
|
e06111a362 | ||
|
|
cd3904046d | ||
|
|
89aaf64209 | ||
|
|
95ae2aeef3 | ||
|
|
cefe274f74 | ||
|
|
d8bc3098db | ||
|
|
628b89009c | ||
|
|
23ff2988bf | ||
|
|
e0c199255f | ||
|
|
b2422b3c8f | ||
|
|
4cc3102758 | ||
|
|
8c020488dd | ||
|
|
0335d479f9 | ||
|
|
dcaab9d6b5 | ||
|
|
0ba45ad82b | ||
|
|
802f8809dd | ||
|
|
7a63947b94 | ||
|
|
737b1723c7 | ||
|
|
e90e254575 | ||
|
|
f8ad74fc42 | ||
|
|
6c862b8fbf | ||
|
|
4b908ea626 | ||
|
|
b77937dfad | ||
|
|
b00e14eb3f | ||
|
|
9748a030f7 | ||
|
|
1060d62add | ||
|
|
4c3dd84b32 | ||
|
|
82657acdce | ||
|
|
c96cc831d5 | ||
|
|
e41a5278f7 | ||
|
|
b5aa562039 | ||
|
|
5281aeb07b | ||
|
|
39717812ee | ||
|
|
9f6e182400 | ||
|
|
e4ba8fea8a | ||
|
|
f80aea60c7 | ||
|
|
a407558375 | ||
|
|
65aa41ab81 | ||
|
|
6331963e66 | ||
|
|
32fa8d5143 | ||
|
|
8f3fcdcf5c | ||
|
|
794919e91d | ||
|
|
667d26ca12 | ||
|
|
50dec12072 | ||
|
|
8fb6b03ddc | ||
|
|
7bfa29fa81 | ||
|
|
9bada6421e | ||
|
|
b14e75dd6c | ||
|
|
6623583004 | ||
|
|
d50098b622 | ||
|
|
642dcd4b70 | ||
|
|
a7d8ed0c6d | ||
|
|
ab1b910d80 | ||
|
|
a3c2ff61ed | ||
|
|
b111434fa9 | ||
|
|
613a087387 | ||
|
|
b0a19a9801 | ||
|
|
086a78f863 | ||
|
|
b4c64a21b3 | ||
|
|
fb3886cf04 | ||
|
|
c9079d4b4d | ||
|
|
c4b441de65 | ||
|
|
f789ad59a9 | ||
|
|
cdf620e6ee | ||
|
|
2993332b38 | ||
|
|
ab1f2ae914 | ||
|
|
68e33b5386 | ||
|
|
e31f680788 | ||
|
|
1bf16ec23d | ||
|
|
2697a4d7f8 | ||
|
|
8629f6b0f3 | ||
|
|
1332a0d381 | ||
|
|
a8859a8145 | ||
|
|
6fedd72e39 | ||
|
|
472a291c24 | ||
|
|
08df5f6d7c | ||
|
|
b124c15018 | ||
|
|
0a7a265ff5 | ||
|
|
93d486d50e | ||
|
|
af10aba805 | ||
|
|
39c6ecb50d | ||
|
|
89b4627e9a | ||
|
|
e920f96b17 | ||
|
|
cf4bbcde14 | ||
|
|
c882aacc23 | ||
|
|
624f2ebcff | ||
|
|
eb218f6674 | ||
|
|
9fdf2cd16c | ||
|
|
385a894b3c | ||
|
|
2d5b685429 | ||
|
|
900dc1f0a3 | ||
|
|
eeb00a5ca2 | ||
|
|
2b913a99a3 | ||
|
|
0994a07874 | ||
|
|
a818d8e3b7 | ||
|
|
3ae7302f7b | ||
|
|
9ba03523e7 | ||
|
|
f4e67bf2d3 | ||
|
|
a3d3ca293e | ||
|
|
d6f3444141 | ||
|
|
0e20b15afe | ||
|
|
e1cc104805 | ||
|
|
de8492de34 | ||
|
|
90d9cdacfa | ||
|
|
13e58c1b64 | ||
|
|
d5a049dc54 | ||
|
|
dec44e4bcd | ||
|
|
ae0554224a | ||
|
|
fe5c4b95d5 | ||
|
|
90fca993f3 | ||
|
|
efefff71d1 | ||
|
|
5da47de6eb | ||
|
|
b032f34831 | ||
|
|
ea4ad3944c | ||
|
|
ea3f873ae9 | ||
|
|
de7e8fd918 | ||
|
|
17560967c7 | ||
|
|
9a4f2b9aa3 | ||
|
|
6f698073bb | ||
|
|
8662108344 | ||
|
|
efe3bca19e | ||
|
|
6898fd7d0f | ||
|
|
5701d6d333 | ||
|
|
074ce356de | ||
|
|
fa885c3346 | ||
|
|
a4249a6351 | ||
|
|
877d21a029 | ||
|
|
6b2ba73701 | ||
|
|
cee08e316d | ||
|
|
8125b0499b | ||
|
|
57b01cf8fb | ||
|
|
fea169a9c0 | ||
|
|
adde37394b | ||
|
|
aea8977d05 | ||
|
|
e6919c3242 | ||
|
|
a560f789e4 | ||
|
|
61c6048b45 | ||
|
|
1692a52a6a | ||
|
|
3e1337d753 | ||
|
|
0fb3c08181 | ||
|
|
dab6396ebe | ||
|
|
c073b8b4ee | ||
|
|
8f72f8def9 | ||
|
|
9ca4727573 | ||
|
|
5465cabd40 | ||
|
|
81715f6553 | ||
|
|
a6a7c548d5 | ||
|
|
003968f06a | ||
|
|
2ee09d9f7d | ||
|
|
9380fcd09a | ||
|
|
32432d6df1 | ||
|
|
1bbecd46c8 | ||
|
|
c658781995 | ||
|
|
84af508525 | ||
|
|
6f792e0558 | ||
|
|
4ef7aff663 | ||
|
|
8f7528a0bc | ||
|
|
5a7f9c81e3 | ||
|
|
886d756200 | ||
|
|
a33214ab6b | ||
|
|
32a90deeaf | ||
|
|
0b271d24d1 | ||
|
|
ea759c4742 | ||
|
|
7837843f82 | ||
|
|
5639ba423b | ||
|
|
918764a4f7 | ||
|
|
e56b5c063c | ||
|
|
a232f1f34e | ||
|
|
9864185b57 | ||
|
|
888ae00867 | ||
|
|
7b5f82ffc7 | ||
|
|
7404494772 | ||
|
|
a29f83c4e7 | ||
|
|
4c352ff974 | ||
|
|
55bd7a1c65 | ||
|
|
9c8c837ab9 | ||
|
|
2c328cc7c9 | ||
|
|
56060db29d | ||
|
|
7d1ec20429 | ||
|
|
ff5f0c3e39 | ||
|
|
9a2e81f5f0 | ||
|
|
f674e28263 | ||
|
|
4a9a88b683 | ||
|
|
b86f8df29f | ||
|
|
61644f216e | ||
|
|
cb0666ac21 | ||
|
|
2f4f4c2376 | ||
|
|
0b1e30988a | ||
|
|
13227fc017 | ||
|
|
cc4598c41b | ||
|
|
e7560a5acf | ||
|
|
745b24f13a | ||
|
|
aee57107bc | ||
|
|
e4febfa097 | ||
|
|
fd3c24af4e | ||
|
|
d0114e0703 | ||
|
|
359f8b67f8 | ||
|
|
6c6be5de88 | ||
|
|
925bfe840b | ||
|
|
55b0ac85d1 | ||
|
|
d4743b1a17 | ||
|
|
4939d6871b | ||
|
|
9d06e3b2cd | ||
|
|
3f3a5bb0ab | ||
|
|
0c3ba5123e | ||
|
|
c513308ded | ||
|
|
1764170307 | ||
|
|
e4c9734fcb | ||
|
|
188b928548 | ||
|
|
e8b11a78d1 | ||
|
|
10e0c81de9 | ||
|
|
d5ab2c6891 | ||
|
|
fdaa6a9ee5 | ||
|
|
c657e2a1bb | ||
|
|
bc82f48ebf | ||
|
|
658944e756 | ||
|
|
ba6cde8a87 | ||
|
|
9bd26d6faa | ||
|
|
a79a6a5c34 | ||
|
|
0c461023fe | ||
|
|
93731e4ca9 | ||
|
|
f72e28a56c | ||
|
|
dbe5d1ca08 | ||
|
|
ca0b7217d2 | ||
|
|
16ce8ab16c | ||
|
|
70a5be08c7 | ||
|
|
b850b8a5a4 | ||
|
|
05af00305d | ||
|
|
2f75eef499 | ||
|
|
3df6fa7ccb | ||
|
|
a5fa1cd835 | ||
|
|
8d149348a0 | ||
|
|
d26569f636 | ||
|
|
93590d224b | ||
|
|
0888ddde7c | ||
|
|
834617ab91 | ||
|
|
b73b8343d5 | ||
|
|
ab94017e3a | ||
|
|
82189066e8 | ||
|
|
3b856b64ed | ||
|
|
63cf80a456 | ||
|
|
91de8e082e | ||
|
|
19c340d3fb | ||
|
|
ea15d91e29 | ||
|
|
5af926bac8 | ||
|
|
e4d6fc65ca | ||
|
|
87ff1a9038 | ||
|
|
f2006c7efe | ||
|
|
66c2acc08d | ||
|
|
fab093395b | ||
|
|
b0ad5cd863 | ||
|
|
fd8998a38f | ||
|
|
052531ac13 | ||
|
|
60e322962e | ||
|
|
e3fa48b6ce | ||
|
|
30182d77cc | ||
|
|
a0126f371e | ||
|
|
ff60754091 | ||
|
|
b99f296494 | ||
|
|
bbda717b69 | ||
|
|
3d0c06ccee | ||
|
|
5e3742f899 | ||
|
|
fb12ee3f52 | ||
|
|
38291e9af2 | ||
|
|
f0feb7bd35 | ||
|
|
e67eb89e05 | ||
|
|
0dae8ded13 | ||
|
|
4045e96e96 | ||
|
|
1aa963a2fe | ||
|
|
8e9b00a017 | ||
|
|
b1bab2ece8 | ||
|
|
aa2b764d74 | ||
|
|
4da220c513 | ||
|
|
74c8690cd1 | ||
|
|
b38acc8559 | ||
|
|
bb72957535 | ||
|
|
3e543691a4 | ||
|
|
2a67d3d6cd | ||
|
|
a13f90fc60 | ||
|
|
c797ef3357 | ||
|
|
0746e2f682 | ||
|
|
8d63ee55f3 | ||
|
|
7705ba16e4 | ||
|
|
3d26bca7c5 | ||
|
|
7037ae2f55 | ||
|
|
2448c4e290 | ||
|
|
fcb1178dc7 | ||
|
|
665b67ff35 | ||
|
|
c70947c967 | ||
|
|
1a2d81bc01 | ||
|
|
6b853ec36b | ||
|
|
9a170790ad | ||
|
|
ca8458024e | ||
|
|
941c787806 | ||
|
|
a8e310429c | ||
|
|
5ca39eb9fd | ||
|
|
8d898f2d12 | ||
|
|
9072959858 | ||
|
|
42119e2c52 | ||
|
|
f376a61578 | ||
|
|
85c22889da | ||
|
|
d3f71930f0 | ||
|
|
2017856791 | ||
|
|
893bfe9d3a | ||
|
|
ceef600223 | ||
|
|
99b50b39fe | ||
|
|
304aed0f13 | ||
|
|
7b37cdcebb | ||
|
|
d9ee53b504 | ||
|
|
fa92abce31 | ||
|
|
b7aeb432ed | ||
|
|
c9a8808b0d | ||
|
|
27d395ba06 | ||
|
|
da757069de | ||
|
|
e1435501aa | ||
|
|
68519d6ca7 | ||
|
|
f20b7c2f33 | ||
|
|
deeb04e695 | ||
|
|
cd27845c75 | ||
|
|
ba4b0db462 | ||
|
|
95d56c737d | ||
|
|
df1ff8f994 | ||
|
|
c315634365 | ||
|
|
70e8337536 | ||
|
|
06062568c7 | ||
|
|
6d62e71c34 | ||
|
|
c47eddce32 | ||
|
|
e98f859a14 | ||
|
|
86074cff6c | ||
|
|
3ac3615ecd | ||
|
|
d590647d09 | ||
|
|
e4d7d41df6 | ||
|
|
a461861030 | ||
|
|
40d4db97e6 | ||
|
|
2c711d8365 | ||
|
|
35f3824932 | ||
|
|
ec6fe9939b | ||
|
|
7bac1a170d | ||
|
|
f85d7e3686 | ||
|
|
93cc1a88ee | ||
|
|
1b581b714f | ||
|
|
22746c7a3f | ||
|
|
f837d2cdbb | ||
|
|
c3df481b22 | ||
|
|
8da33721d5 | ||
|
|
a84e488a4e | ||
|
|
77ae73e659 | ||
|
|
2452e271cd | ||
|
|
8dafe3cba8 | ||
|
|
4e8b390682 |
@@ -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
|
||||||
|
|||||||
+94
-4
@@ -5,6 +5,100 @@ 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🌐 Full Context Mode for Web Search**: Enable highly accurate web searches by utilizing full context mode—ideal for models with large context windows, ensuring more precise and insightful results.
|
||||||
|
- **⚡ Optimized Asynchronous Web Search**: Web searches now load significantly faster with optimized async support, providing users with quicker, more efficient information retrieval.
|
||||||
|
- **🔄 Auto Text Direction for RTL Languages**: Automatic text alignment based on language input, ensuring seamless conversation flow for Arabic, Hebrew, and other right-to-left scripts.
|
||||||
|
- **🚀 Jupyter Notebook Support for Code Execution**: The "Run" button in code blocks can now use Jupyter for execution, offering a powerful, dynamic coding experience directly in the chat.
|
||||||
|
- **🗑️ Message Delete Confirmation Dialog**: Prevent accidental deletions with a new confirmation prompt before removing messages, adding an additional layer of security to your chat history.
|
||||||
|
- **📥 Download Button for SVG Diagrams**: SVG diagrams generated within chat can now be downloaded instantly, making it easier to save and share complex visual data.
|
||||||
|
- **✨ General UI/UX Improvements and Backend Stability**: A refined interface with smoother interactions, improved layouts, and backend stability enhancements for a more reliable, polished experience.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🛠️ Temporary Chat Message Continue Button Fixed**: The "Continue Response" button for temporary chats now works as expected, ensuring an uninterrupted conversation flow.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **📝 Prompt Variable Update**: Deprecated square bracket '[]' indicators for prompt variables; now requires double curly brackets '{{}}' for consistency and clarity.
|
||||||
|
- **🔧 Stability Enhancements**: Error handling improved in chat history, ensuring smoother operations when reviewing previous messages.
|
||||||
|
|
||||||
|
## [0.5.12] - 2025-02-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🛠️ Multiple Tool Calls Support for Native Function Mode**: Functions now can call multiple tools within a single response, unlocking better automation and workflow flexibility when using native function calling.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **📝 Playground Text Completion Restored**: Addressed an issue where text completion in the Playground was not functioning.
|
||||||
|
- **🔗 Direct Connections Now Work for Regular Users**: Fixed a bug where users with the 'user' role couldn't establish direct API connections, enabling seamless model usage for all user tiers.
|
||||||
|
- **⚡ Landing Page Input No Longer Lags with Long Text**: Improved input responsiveness on the landing page, ensuring fast and smooth typing experiences even when entering long messages.
|
||||||
|
- **🔧 Parameter in Functions Fixed**: Fixed an issue where the reserved parameters wasn’t recognized within functions, restoring full functionality for advanced task-based automation.
|
||||||
|
|
||||||
## [0.5.11] - 2025-02-13
|
## [0.5.11] - 2025-02-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -29,10 +123,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **💾 User Settings Persistence Fix**: Ensured user-specific settings are correctly saved and applied across sessions.
|
- **💾 User Settings Persistence Fix**: Ensured user-specific settings are correctly saved and applied across sessions.
|
||||||
- **🔄 OpenID Username Retrieval Enhancement**: Usernames are now correctly picked up and assigned for OpenID Connect (OIDC) logins.
|
- **🔄 OpenID Username Retrieval Enhancement**: Usernames are now correctly picked up and assigned for OpenID Connect (OIDC) logins.
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- **🔗 Improved Direct Connections Integration**: Simplified the configuration process for setting up direct API connections, making it easier to integrate custom inference endpoints.
|
|
||||||
|
|
||||||
## [0.5.10] - 2025-02-05
|
## [0.5.10] - 2025-02-05
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -13,10 +13,15 @@
|
|||||||
|
|
||||||
**Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
|
**Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
|
||||||
|
|
||||||
For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:sales@openwebui.com)**
|
||||||
|
>
|
||||||
|
> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
|
||||||
|
|
||||||
|
For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||||
|
|
||||||
## Key Features of Open WebUI ⭐
|
## Key Features of Open WebUI ⭐
|
||||||
|
|
||||||
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
|
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
|
||||||
|
|||||||
+223
-29
@@ -2,12 +2,13 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import base64
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
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
|
||||||
@@ -42,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
|
||||||
@@ -55,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()
|
||||||
@@ -593,8 +594,6 @@ if frontend_favicon.exists():
|
|||||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"An error occurred: {e}")
|
logging.error(f"An error occurred: {e}")
|
||||||
else:
|
|
||||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
|
||||||
|
|
||||||
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
|
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
|
||||||
|
|
||||||
@@ -603,12 +602,18 @@ if frontend_splash.exists():
|
|||||||
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
|
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"An error occurred: {e}")
|
logging.error(f"An error occurred: {e}")
|
||||||
else:
|
|
||||||
logging.warning(f"Frontend splash not found at {frontend_splash}")
|
frontend_loader = FRONTEND_BUILD_DIR / "static" / "loader.js"
|
||||||
|
|
||||||
|
if frontend_loader.exists():
|
||||||
|
try:
|
||||||
|
shutil.copyfile(frontend_loader, STATIC_DIR / "loader.js")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# CUSTOM_NAME
|
# CUSTOM_NAME (Legacy)
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
||||||
@@ -650,6 +655,16 @@ if CUSTOM_NAME:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# LICENSE_KEY
|
||||||
|
####################################
|
||||||
|
|
||||||
|
LICENSE_KEY = PersistentConfig(
|
||||||
|
"LICENSE_KEY",
|
||||||
|
"license.key",
|
||||||
|
os.environ.get("LICENSE_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# STORAGE PROVIDER
|
# STORAGE PROVIDER
|
||||||
####################################
|
####################################
|
||||||
@@ -662,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
|
||||||
####################################
|
####################################
|
||||||
@@ -767,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"
|
||||||
@@ -1071,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)
|
||||||
@@ -1190,6 +1216,12 @@ ENABLE_TAGS_GENERATION = PersistentConfig(
|
|||||||
os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
|
os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENABLE_TITLE_GENERATION = PersistentConfig(
|
||||||
|
"ENABLE_TITLE_GENERATION",
|
||||||
|
"task.title.enable",
|
||||||
|
os.environ.get("ENABLE_TITLE_GENERATION", "True").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
|
ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
|
||||||
"ENABLE_SEARCH_QUERY_GENERATION",
|
"ENABLE_SEARCH_QUERY_GENERATION",
|
||||||
@@ -1341,6 +1373,44 @@ Responses from models: {{responses}}"""
|
|||||||
# Code Interpreter
|
# Code Interpreter
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
|
|
||||||
|
CODE_EXECUTION_ENGINE = PersistentConfig(
|
||||||
|
"CODE_EXECUTION_ENGINE",
|
||||||
|
"code_execution.engine",
|
||||||
|
os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"),
|
||||||
|
)
|
||||||
|
|
||||||
|
CODE_EXECUTION_JUPYTER_URL = PersistentConfig(
|
||||||
|
"CODE_EXECUTION_JUPYTER_URL",
|
||||||
|
"code_execution.jupyter.url",
|
||||||
|
os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig(
|
||||||
|
"CODE_EXECUTION_JUPYTER_AUTH",
|
||||||
|
"code_execution.jupyter.auth",
|
||||||
|
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig(
|
||||||
|
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN",
|
||||||
|
"code_execution.jupyter.auth_token",
|
||||||
|
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig(
|
||||||
|
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD",
|
||||||
|
"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",
|
||||||
"code_interpreter.enable",
|
"code_interpreter.enable",
|
||||||
@@ -1362,26 +1432,48 @@ CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig(
|
|||||||
CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
|
CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
|
||||||
"CODE_INTERPRETER_JUPYTER_URL",
|
"CODE_INTERPRETER_JUPYTER_URL",
|
||||||
"code_interpreter.jupyter.url",
|
"code_interpreter.jupyter.url",
|
||||||
os.environ.get("CODE_INTERPRETER_JUPYTER_URL", ""),
|
os.environ.get(
|
||||||
|
"CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
|
CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
|
||||||
"CODE_INTERPRETER_JUPYTER_AUTH",
|
"CODE_INTERPRETER_JUPYTER_AUTH",
|
||||||
"code_interpreter.jupyter.auth",
|
"code_interpreter.jupyter.auth",
|
||||||
os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH", ""),
|
os.environ.get(
|
||||||
|
"CODE_INTERPRETER_JUPYTER_AUTH",
|
||||||
|
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
|
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
|
||||||
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
|
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
|
||||||
"code_interpreter.jupyter.auth_token",
|
"code_interpreter.jupyter.auth_token",
|
||||||
os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", ""),
|
os.environ.get(
|
||||||
|
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
|
||||||
|
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
|
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
|
||||||
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
|
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
|
||||||
"code_interpreter.jupyter.auth_password",
|
"code_interpreter.jupyter.auth_password",
|
||||||
os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", ""),
|
os.environ.get(
|
||||||
|
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
|
||||||
|
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1409,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
|
||||||
@@ -1477,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",
|
||||||
@@ -1490,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"))
|
||||||
)
|
)
|
||||||
@@ -1505,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",
|
||||||
@@ -1619,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.
|
||||||
@@ -1700,6 +1836,12 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
|
|||||||
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
|
||||||
|
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL",
|
||||||
|
"rag.web.search.bypass_embedding_and_retrieval",
|
||||||
|
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.
|
||||||
# This ensures the highest level of safety and reliability of the information sources.
|
# This ensures the highest level of safety and reliability of the information sources.
|
||||||
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
|
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
|
||||||
@@ -1803,6 +1945,18 @@ SEARCHAPI_ENGINE = PersistentConfig(
|
|||||||
os.getenv("SEARCHAPI_ENGINE", ""),
|
os.getenv("SEARCHAPI_ENGINE", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SERPAPI_API_KEY = PersistentConfig(
|
||||||
|
"SERPAPI_API_KEY",
|
||||||
|
"rag.web.search.serpapi_api_key",
|
||||||
|
os.getenv("SERPAPI_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
SERPAPI_ENGINE = PersistentConfig(
|
||||||
|
"SERPAPI_ENGINE",
|
||||||
|
"rag.web.search.serpapi_engine",
|
||||||
|
os.getenv("SERPAPI_ENGINE", ""),
|
||||||
|
)
|
||||||
|
|
||||||
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
|
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
|
||||||
"BING_SEARCH_V7_ENDPOINT",
|
"BING_SEARCH_V7_ENDPOINT",
|
||||||
"rag.web.search.bing_search_v7_endpoint",
|
"rag.web.search.bing_search_v7_endpoint",
|
||||||
@@ -1835,6 +1989,35 @@ 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",
|
||||||
|
"rag.web.search.trust_env",
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Images
|
# Images
|
||||||
@@ -2046,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")
|
||||||
)
|
)
|
||||||
@@ -2222,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(
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ if WEBUI_NAME != "Open WebUI":
|
|||||||
|
|
||||||
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||||
|
|
||||||
|
TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# ENV (dev,test,prod)
|
# ENV (dev,test,prod)
|
||||||
@@ -418,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]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+111
-21
@@ -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,
|
||||||
@@ -88,18 +91,26 @@ from open_webui.models.models import Models
|
|||||||
from open_webui.models.users import UserModel, Users
|
from open_webui.models.users import UserModel, Users
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
|
LICENSE_KEY,
|
||||||
# Ollama
|
# Ollama
|
||||||
ENABLE_OLLAMA_API,
|
ENABLE_OLLAMA_API,
|
||||||
OLLAMA_BASE_URLS,
|
OLLAMA_BASE_URLS,
|
||||||
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,
|
||||||
# Direct Connections
|
# Direct Connections
|
||||||
ENABLE_DIRECT_CONNECTIONS,
|
ENABLE_DIRECT_CONNECTIONS,
|
||||||
# Code Interpreter
|
# Code Execution
|
||||||
|
CODE_EXECUTION_ENGINE,
|
||||||
|
CODE_EXECUTION_JUPYTER_URL,
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH,
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
|
||||||
|
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,
|
||||||
@@ -107,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,
|
||||||
@@ -125,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,
|
||||||
@@ -139,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,
|
||||||
@@ -146,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,
|
||||||
@@ -165,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,
|
||||||
@@ -173,12 +195,16 @@ 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,
|
||||||
|
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_DOMAIN_FILTER_LIST,
|
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
JINA_API_KEY,
|
JINA_API_KEY,
|
||||||
SEARCHAPI_API_KEY,
|
SEARCHAPI_API_KEY,
|
||||||
SEARCHAPI_ENGINE,
|
SEARCHAPI_ENGINE,
|
||||||
|
SERPAPI_API_KEY,
|
||||||
|
SERPAPI_ENGINE,
|
||||||
SEARXNG_QUERY_URL,
|
SEARXNG_QUERY_URL,
|
||||||
SERPER_API_KEY,
|
SERPER_API_KEY,
|
||||||
SERPLY_API_KEY,
|
SERPLY_API_KEY,
|
||||||
@@ -196,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,
|
||||||
@@ -264,6 +292,7 @@ from open_webui.config import (
|
|||||||
TASK_MODEL,
|
TASK_MODEL,
|
||||||
TASK_MODEL_EXTERNAL,
|
TASK_MODEL_EXTERNAL,
|
||||||
ENABLE_TAGS_GENERATION,
|
ENABLE_TAGS_GENERATION,
|
||||||
|
ENABLE_TITLE_GENERATION,
|
||||||
ENABLE_SEARCH_QUERY_GENERATION,
|
ENABLE_SEARCH_QUERY_GENERATION,
|
||||||
ENABLE_RETRIEVAL_QUERY_GENERATION,
|
ENABLE_RETRIEVAL_QUERY_GENERATION,
|
||||||
ENABLE_AUTOCOMPLETE_GENERATION,
|
ENABLE_AUTOCOMPLETE_GENERATION,
|
||||||
@@ -278,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,
|
||||||
@@ -310,15 +342,17 @@ from open_webui.utils.middleware import process_chat_payload, process_chat_respo
|
|||||||
from open_webui.utils.access_control import has_access
|
from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
from open_webui.utils.auth import (
|
from open_webui.utils.auth import (
|
||||||
|
get_license_data,
|
||||||
decode_token,
|
decode_token,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
)
|
)
|
||||||
from open_webui.utils.oauth import oauth_manager
|
from open_webui.utils.oauth import OAuthManager
|
||||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||||
|
|
||||||
from open_webui.tasks import stop_task, list_tasks # Import from tasks.py
|
from open_webui.tasks import stop_task, list_tasks # Import from tasks.py
|
||||||
|
|
||||||
|
|
||||||
if SAFE_MODE:
|
if SAFE_MODE:
|
||||||
print("SAFE MODE ENABLED")
|
print("SAFE MODE ENABLED")
|
||||||
Functions.deactivate_all_functions()
|
Functions.deactivate_all_functions()
|
||||||
@@ -345,12 +379,12 @@ class SPAStaticFiles(StaticFiles):
|
|||||||
|
|
||||||
print(
|
print(
|
||||||
rf"""
|
rf"""
|
||||||
___ __ __ _ _ _ ___
|
██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗
|
||||||
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
|
██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║
|
||||||
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
|
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║
|
||||||
| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || |
|
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║███╗██║██╔══╝ ██╔══██╗██║ ██║██║
|
||||||
\___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___|
|
╚██████╔╝██║ ███████╗██║ ╚████║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║
|
||||||
|_|
|
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝
|
||||||
|
|
||||||
|
|
||||||
v{VERSION} - building the best open-source AI user interface.
|
v{VERSION} - building the best open-source AI user interface.
|
||||||
@@ -362,9 +396,13 @@ 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()
|
||||||
|
|
||||||
|
if app.state.config.LICENSE_KEY:
|
||||||
|
get_license_data(app, app.state.config.LICENSE_KEY)
|
||||||
|
|
||||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -376,8 +414,12 @@ app = FastAPI(
|
|||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
oauth_manager = OAuthManager(app)
|
||||||
|
|
||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
|
|
||||||
|
app.state.WEBUI_NAME = WEBUI_NAME
|
||||||
|
app.state.config.LICENSE_KEY = LICENSE_KEY
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
#
|
#
|
||||||
@@ -479,10 +521,10 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
|
|||||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
||||||
|
|
||||||
|
app.state.USER_COUNT = None
|
||||||
app.state.TOOLS = {}
|
app.state.TOOLS = {}
|
||||||
app.state.FUNCTIONS = {}
|
app.state.FUNCTIONS = {}
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
#
|
#
|
||||||
# RETRIEVAL
|
# RETRIEVAL
|
||||||
@@ -495,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
|
||||||
@@ -502,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
|
||||||
@@ -529,9 +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.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
|
||||||
@@ -546,6 +597,8 @@ app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
|||||||
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
|
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
|
||||||
app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
|
app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
|
||||||
app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
|
app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
|
||||||
|
app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
|
||||||
|
app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
|
||||||
app.state.config.JINA_API_KEY = JINA_API_KEY
|
app.state.config.JINA_API_KEY = JINA_API_KEY
|
||||||
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
||||||
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
||||||
@@ -553,6 +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.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
|
||||||
@@ -596,10 +654,19 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
|||||||
|
|
||||||
########################################
|
########################################
|
||||||
#
|
#
|
||||||
# CODE INTERPRETER
|
# CODE EXECUTION
|
||||||
#
|
#
|
||||||
########################################
|
########################################
|
||||||
|
|
||||||
|
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
|
||||||
|
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
|
||||||
|
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
|
||||||
|
app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN
|
||||||
|
app.state.config.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
|
||||||
app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
|
app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
|
||||||
@@ -612,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
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
#
|
#
|
||||||
@@ -626,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
|
||||||
@@ -689,6 +760,7 @@ app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION
|
|||||||
app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
|
app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
|
||||||
app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
|
app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
|
||||||
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
||||||
|
app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION
|
||||||
|
|
||||||
|
|
||||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
@@ -826,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
|
||||||
@@ -858,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 = [
|
||||||
@@ -887,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}
|
||||||
|
|
||||||
|
|
||||||
@@ -898,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)
|
||||||
@@ -934,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"}
|
||||||
@@ -1063,7 +1148,7 @@ async def get_app_config(request: Request):
|
|||||||
return {
|
return {
|
||||||
**({"onboarding": True} if onboarding else {}),
|
**({"onboarding": True} if onboarding else {}),
|
||||||
"status": True,
|
"status": True,
|
||||||
"name": WEBUI_NAME,
|
"name": app.state.WEBUI_NAME,
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
"default_locale": str(DEFAULT_LOCALE),
|
"default_locale": str(DEFAULT_LOCALE),
|
||||||
"oauth": {
|
"oauth": {
|
||||||
@@ -1093,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 {}
|
||||||
@@ -1102,6 +1188,9 @@ async def get_app_config(request: Request):
|
|||||||
{
|
{
|
||||||
"default_models": app.state.config.DEFAULT_MODELS,
|
"default_models": app.state.config.DEFAULT_MODELS,
|
||||||
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
|
"code": {
|
||||||
|
"engine": app.state.config.CODE_EXECUTION_ENGINE,
|
||||||
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"tts": {
|
"tts": {
|
||||||
"engine": app.state.config.TTS_ENGINE,
|
"engine": app.state.config.TTS_ENGINE,
|
||||||
@@ -1121,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 {}
|
||||||
@@ -1198,7 +1288,7 @@ if len(OAUTH_PROVIDERS) > 0:
|
|||||||
|
|
||||||
@app.get("/oauth/{provider}/login")
|
@app.get("/oauth/{provider}/login")
|
||||||
async def oauth_login(provider: str, request: Request):
|
async def oauth_login(provider: str, request: Request):
|
||||||
return await oauth_manager.handle_login(provider, request)
|
return await oauth_manager.handle_login(request, provider)
|
||||||
|
|
||||||
|
|
||||||
# OAuth login logic is as follows:
|
# OAuth login logic is as follows:
|
||||||
@@ -1209,14 +1299,14 @@ async def oauth_login(provider: str, request: Request):
|
|||||||
# - Email addresses are considered unique, so we fail registration if the email address is already taken
|
# - Email addresses are considered unique, so we fail registration if the email address is already taken
|
||||||
@app.get("/oauth/{provider}/callback")
|
@app.get("/oauth/{provider}/callback")
|
||||||
async def oauth_callback(provider: str, request: Request, response: Response):
|
async def oauth_callback(provider: str, request: Request, response: Response):
|
||||||
return await oauth_manager.handle_callback(provider, request, response)
|
return await oauth_manager.handle_callback(request, provider, response)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/manifest.json")
|
@app.get("/manifest.json")
|
||||||
async def get_manifest_json():
|
async def get_manifest_json():
|
||||||
return {
|
return {
|
||||||
"name": WEBUI_NAME,
|
"name": app.state.WEBUI_NAME,
|
||||||
"short_name": WEBUI_NAME,
|
"short_name": app.state.WEBUI_NAME,
|
||||||
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
|
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
@@ -1243,8 +1333,8 @@ async def get_manifest_json():
|
|||||||
async def get_opensearch_xml():
|
async def get_opensearch_xml():
|
||||||
xml_content = rf"""
|
xml_content = rf"""
|
||||||
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
||||||
<ShortName>{WEBUI_NAME}</ShortName>
|
<ShortName>{app.state.WEBUI_NAME}</ShortName>
|
||||||
<Description>Search {WEBUI_NAME}</Description>
|
<Description>Search {app.state.WEBUI_NAME}</Description>
|
||||||
<InputEncoding>UTF-8</InputEncoding>
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
<Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
|
<Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
|
||||||
<Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>
|
<Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
Regular → Executable
+2
-3
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,21 +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:
|
||||||
if file.get("context") == "full":
|
|
||||||
|
context = None
|
||||||
|
if file.get("docs"):
|
||||||
|
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
|
context = {
|
||||||
|
"documents": [[doc.get("content") for doc in file.get("docs")]],
|
||||||
|
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
|
||||||
|
}
|
||||||
|
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"):
|
||||||
@@ -331,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 = []
|
||||||
@@ -463,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
|
||||||
|
|
||||||
|
|
||||||
@@ -497,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+6
-2
@@ -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]:
|
||||||
|
|||||||
@@ -32,19 +32,15 @@ def search_duckduckgo(
|
|||||||
# Convert the search results into a list
|
# Convert the search results into a list
|
||||||
search_results = [r for r in ddgs_gen]
|
search_results = [r for r in ddgs_gen]
|
||||||
|
|
||||||
# Create an empty list to store the SearchResult objects
|
|
||||||
results = []
|
|
||||||
# Iterate over each search result
|
|
||||||
for result in search_results:
|
|
||||||
# Create a SearchResult object and append it to the results list
|
|
||||||
results.append(
|
|
||||||
SearchResult(
|
|
||||||
link=result["href"],
|
|
||||||
title=result.get("title"),
|
|
||||||
snippet=result.get("body"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if filter_list:
|
if filter_list:
|
||||||
results = get_filtered_results(results, filter_list)
|
search_results = get_filtered_results(search_results, filter_list)
|
||||||
|
|
||||||
# Return the list of search results
|
# Return the list of search results
|
||||||
return results
|
return [
|
||||||
|
SearchResult(
|
||||||
|
link=result["href"],
|
||||||
|
title=result.get("title"),
|
||||||
|
snippet=result.get("body"),
|
||||||
|
)
|
||||||
|
for result in search_results
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
|
def search_serpapi(
|
||||||
|
api_key: str,
|
||||||
|
engine: str,
|
||||||
|
query: str,
|
||||||
|
count: int,
|
||||||
|
filter_list: Optional[list[str]] = None,
|
||||||
|
) -> list[SearchResult]:
|
||||||
|
"""Search using serpapi.com's API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key (str): A serpapi.com API key
|
||||||
|
query (str): The query to search for
|
||||||
|
"""
|
||||||
|
url = "https://serpapi.com/search"
|
||||||
|
|
||||||
|
engine = engine or "google"
|
||||||
|
|
||||||
|
payload = {"engine": engine, "q": query, "api_key": api_key}
|
||||||
|
|
||||||
|
url = f"{url}?{urlencode(payload)}"
|
||||||
|
response = requests.request("GET", url)
|
||||||
|
|
||||||
|
json_response = response.json()
|
||||||
|
log.info(f"results from serpapi search: {json_response}")
|
||||||
|
|
||||||
|
results = sorted(
|
||||||
|
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
|
||||||
|
)
|
||||||
|
if filter_list:
|
||||||
|
results = get_filtered_results(results, filter_list)
|
||||||
|
return [
|
||||||
|
SearchResult(
|
||||||
|
link=result["link"], title=result["title"], snippet=result["snippet"]
|
||||||
|
)
|
||||||
|
for result in results[:count]
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from open_webui.retrieval.web.main import SearchResult
|
from open_webui.retrieval.web.main import SearchResult
|
||||||
@@ -8,7 +9,13 @@ log = logging.getLogger(__name__)
|
|||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
|
def search_tavily(
|
||||||
|
api_key: str,
|
||||||
|
query: str,
|
||||||
|
count: int,
|
||||||
|
filter_list: Optional[list[str]] = None,
|
||||||
|
# **kwargs,
|
||||||
|
) -> list[SearchResult]:
|
||||||
"""Search using Tavily's Search API and return the results as a list of SearchResult objects.
|
"""Search using Tavily's Search API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -20,7 +27,6 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
|
|||||||
"""
|
"""
|
||||||
url = "https://api.tavily.com/search"
|
url = "https://api.tavily.com/search"
|
||||||
data = {"query": query, "api_key": api_key}
|
data = {"query": query, "api_key": api_key}
|
||||||
|
|
||||||
response = requests.post(url, json=data)
|
response = requests.post(url, json=data)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
import socket
|
import asyncio
|
||||||
import urllib.parse
|
|
||||||
import validators
|
|
||||||
from typing import Union, Sequence, Iterator
|
|
||||||
|
|
||||||
from langchain_community.document_loaders import (
|
|
||||||
WebBaseLoader,
|
|
||||||
)
|
|
||||||
from langchain_core.documents import Document
|
|
||||||
|
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
|
||||||
from open_webui.config import ENABLE_RAG_LOCAL_WEB_FETCH
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
|
||||||
|
|
||||||
import logging
|
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"])
|
||||||
@@ -65,9 +84,381 @@ 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."""
|
||||||
|
|
||||||
|
def __init__(self, trust_env: bool = False, *args, **kwargs):
|
||||||
|
"""Initialize SafeWebBaseLoader
|
||||||
|
Args:
|
||||||
|
trust_env (bool, optional): set to True if using proxy to make web requests, for example
|
||||||
|
using http(s)_proxy environment variables. Defaults to False.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.trust_env = trust_env
|
||||||
|
|
||||||
|
async def _fetch(
|
||||||
|
self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5
|
||||||
|
) -> str:
|
||||||
|
async with aiohttp.ClientSession(trust_env=self.trust_env) as session:
|
||||||
|
for i in range(retries):
|
||||||
|
try:
|
||||||
|
kwargs: Dict = dict(
|
||||||
|
headers=self.session.headers,
|
||||||
|
cookies=self.session.cookies.get_dict(),
|
||||||
|
)
|
||||||
|
if not self.session.verify:
|
||||||
|
kwargs["ssl"] = False
|
||||||
|
|
||||||
|
async with session.get(
|
||||||
|
url, **(self.requests_kwargs | kwargs)
|
||||||
|
) as response:
|
||||||
|
if self.raise_for_status:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.text()
|
||||||
|
except aiohttp.ClientConnectionError as e:
|
||||||
|
if i == retries - 1:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f"Error fetching {url} with attempt "
|
||||||
|
f"{i + 1}/{retries}: {e}. Retrying..."
|
||||||
|
)
|
||||||
|
await asyncio.sleep(cooldown * backoff**i)
|
||||||
|
raise ValueError("retry count exceeded")
|
||||||
|
|
||||||
|
def _unpack_fetch_results(
|
||||||
|
self, results: Any, urls: List[str], parser: Union[str, None] = None
|
||||||
|
) -> List[Any]:
|
||||||
|
"""Unpack fetch results into BeautifulSoup objects."""
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
final_results = []
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
url = urls[i]
|
||||||
|
if parser is None:
|
||||||
|
if url.endswith(".xml"):
|
||||||
|
parser = "xml"
|
||||||
|
else:
|
||||||
|
parser = self.default_parser
|
||||||
|
self._check_parser(parser)
|
||||||
|
final_results.append(BeautifulSoup(result, parser, **self.bs_kwargs))
|
||||||
|
return final_results
|
||||||
|
|
||||||
|
async def ascrape_all(
|
||||||
|
self, urls: List[str], parser: Union[str, None] = None
|
||||||
|
) -> List[Any]:
|
||||||
|
"""Async fetch all urls, then return soups for all results."""
|
||||||
|
results = await self.fetch_all(urls)
|
||||||
|
return self._unpack_fetch_results(results, urls, parser=parser)
|
||||||
|
|
||||||
def lazy_load(self) -> Iterator[Document]:
|
def lazy_load(self) -> Iterator[Document]:
|
||||||
"""Lazy load text from the url(s) in web_path with error handling."""
|
"""Lazy load text from the url(s) in web_path with error handling."""
|
||||||
for path in self.web_paths:
|
for path in self.web_paths:
|
||||||
@@ -76,33 +467,72 @@ 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 lazy load text from the url(s) in web_path."""
|
||||||
|
results = await self.ascrape_all(self.web_paths)
|
||||||
|
for path, soup in zip(self.web_paths, results):
|
||||||
|
text = soup.get_text(**self.bs_get_text_kwargs)
|
||||||
|
metadata = {"source": path}
|
||||||
|
if title := soup.find("title"):
|
||||||
|
metadata["title"] = title.get_text()
|
||||||
|
if description := soup.find("meta", attrs={"name": "description"}):
|
||||||
|
metadata["description"] = description.get(
|
||||||
|
"content", "No description found."
|
||||||
|
)
|
||||||
|
if html := soup.find("html"):
|
||||||
|
metadata["language"] = html.get("lang", "No language found.")
|
||||||
|
yield Document(page_content=text, metadata=metadata)
|
||||||
|
|
||||||
|
async def aload(self) -> list[Document]:
|
||||||
|
"""Load data into Document objects."""
|
||||||
|
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,
|
||||||
requests_per_second: int = 2,
|
requests_per_second: int = 2,
|
||||||
|
trust_env: bool = False,
|
||||||
):
|
):
|
||||||
# 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 = {
|
||||||
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -251,9 +250,11 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
user = Users.get_user_by_email(mail)
|
user = Users.get_user_by_email(mail)
|
||||||
if not user:
|
if not user:
|
||||||
try:
|
try:
|
||||||
|
user_count = Users.get_num_users()
|
||||||
|
|
||||||
role = (
|
role = (
|
||||||
"admin"
|
"admin"
|
||||||
if Users.get_num_users() == 0
|
if user_count == 0
|
||||||
else request.app.state.config.DEFAULT_USER_ROLE
|
else request.app.state.config.DEFAULT_USER_ROLE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -413,6 +414,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||||||
|
|
||||||
@router.post("/signup", response_model=SessionUserResponse)
|
@router.post("/signup", response_model=SessionUserResponse)
|
||||||
async def signup(request: Request, response: Response, form_data: SignupForm):
|
async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||||
|
|
||||||
if WEBUI_AUTH:
|
if WEBUI_AUTH:
|
||||||
if (
|
if (
|
||||||
not request.app.state.config.ENABLE_SIGNUP
|
not request.app.state.config.ENABLE_SIGNUP
|
||||||
@@ -427,6 +429,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_count = Users.get_num_users()
|
||||||
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
|
||||||
@@ -437,12 +440,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
role = (
|
role = (
|
||||||
"admin"
|
"admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE
|
||||||
if Users.get_num_users() == 0
|
|
||||||
else request.app.state.config.DEFAULT_USER_ROLE
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if Users.get_num_users() == 0:
|
if user_count == 0:
|
||||||
# Disable signup after the first user is created
|
# Disable signup after the first user is created
|
||||||
request.app.state.config.ENABLE_SIGNUP = False
|
request.app.state.config.ENABLE_SIGNUP = False
|
||||||
|
|
||||||
@@ -484,6 +485,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||||||
|
|
||||||
if request.app.state.config.WEBHOOK_URL:
|
if request.app.state.config.WEBHOOK_URL:
|
||||||
post_webhook(
|
post_webhook(
|
||||||
|
request.app.state.WEBUI_NAME,
|
||||||
request.app.state.config.WEBHOOK_URL,
|
request.app.state.config.WEBHOOK_URL,
|
||||||
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||||
{
|
{
|
||||||
@@ -530,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(
|
||||||
@@ -596,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)
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ async def get_channel_messages(
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
async def send_notification(webui_url, channel, message, active_user_ids):
|
async def send_notification(name, webui_url, channel, message, active_user_ids):
|
||||||
users = get_users_with_access("read", channel.access_control)
|
users = get_users_with_access("read", channel.access_control)
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
@@ -206,6 +206,7 @@ async def send_notification(webui_url, channel, message, active_user_ids):
|
|||||||
|
|
||||||
if webhook_url:
|
if webhook_url:
|
||||||
post_webhook(
|
post_webhook(
|
||||||
|
name,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}",
|
f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}",
|
||||||
{
|
{
|
||||||
@@ -302,6 +303,7 @@ async def post_new_message(
|
|||||||
|
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
send_notification,
|
send_notification,
|
||||||
|
request.app.state.WEBUI_NAME,
|
||||||
request.app.state.config.WEBUI_URL,
|
request.app.state.config.WEBUI_URL,
|
||||||
channel,
|
channel,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ async def set_direct_connections_config(
|
|||||||
# CodeInterpreterConfig
|
# CodeInterpreterConfig
|
||||||
############################
|
############################
|
||||||
class CodeInterpreterConfigForm(BaseModel):
|
class CodeInterpreterConfigForm(BaseModel):
|
||||||
|
CODE_EXECUTION_ENGINE: str
|
||||||
|
CODE_EXECUTION_JUPYTER_URL: Optional[str]
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
|
||||||
|
CODE_EXECUTION_JUPYTER_AUTH_TOKEN: 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]
|
||||||
@@ -77,11 +83,18 @@ 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_interpreter", response_model=CodeInterpreterConfigForm)
|
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
|
||||||
async def get_code_interpreter_config(request: Request, user=Depends(get_admin_user)):
|
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
|
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
||||||
|
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||||
|
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
||||||
|
"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_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,
|
||||||
@@ -89,13 +102,32 @@ async def get_code_interpreter_config(request: Request, user=Depends(get_admin_u
|
|||||||
"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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/code_interpreter", response_model=CodeInterpreterConfigForm)
|
@router.post("/code_execution", response_model=CodeInterpreterConfigForm)
|
||||||
async def set_code_interpreter_config(
|
async def set_code_execution_config(
|
||||||
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
|
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
|
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
|
||||||
|
form_data.CODE_EXECUTION_JUPYTER_URL
|
||||||
|
)
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = (
|
||||||
|
form_data.CODE_EXECUTION_JUPYTER_AUTH
|
||||||
|
)
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = (
|
||||||
|
form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
|
||||||
|
)
|
||||||
|
request.app.state.config.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
|
||||||
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
|
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
|
||||||
@@ -116,8 +148,17 @@ async def set_code_interpreter_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_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||||
|
"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_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,
|
||||||
@@ -125,6 +166,7 @@ async def set_code_interpreter_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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Regular → Executable
+11
-5
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,20 +1041,34 @@ 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
|
||||||
messages: list[ChatMessage]
|
messages: list[ChatMessage]
|
||||||
format: Optional[dict] = None
|
format: Optional[Union[dict, str]] = None
|
||||||
options: Optional[dict] = None
|
options: Optional[dict] = None
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
stream: Optional[bool] = True
|
stream: Optional[bool] = True
|
||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from fastapi import (
|
|||||||
status,
|
status,
|
||||||
APIRouter,
|
APIRouter,
|
||||||
)
|
)
|
||||||
|
import aiohttp
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
@@ -56,96 +57,103 @@ def get_sorted_filters(model_id, models):
|
|||||||
return sorted_filters
|
return sorted_filters
|
||||||
|
|
||||||
|
|
||||||
def process_pipeline_inlet_filter(request, payload, user, models):
|
async def process_pipeline_inlet_filter(request, payload, user, models):
|
||||||
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
|
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
|
||||||
model_id = payload["model"]
|
model_id = payload["model"]
|
||||||
|
|
||||||
sorted_filters = get_sorted_filters(model_id, models)
|
sorted_filters = get_sorted_filters(model_id, models)
|
||||||
model = models[model_id]
|
model = models[model_id]
|
||||||
|
|
||||||
if "pipeline" in model:
|
if "pipeline" in model:
|
||||||
sorted_filters.append(model)
|
sorted_filters.append(model)
|
||||||
|
|
||||||
for filter in sorted_filters:
|
async with aiohttp.ClientSession() as session:
|
||||||
r = None
|
for filter in sorted_filters:
|
||||||
try:
|
urlIdx = filter.get("urlIdx")
|
||||||
urlIdx = filter["urlIdx"]
|
if urlIdx is None:
|
||||||
|
continue
|
||||||
|
|
||||||
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||||
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
|
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||||
|
|
||||||
if key == "":
|
if not key:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {key}"}
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
r = requests.post(
|
request_data = {
|
||||||
f"{url}/{filter['id']}/filter/inlet",
|
"user": user,
|
||||||
headers=headers,
|
"body": payload,
|
||||||
json={
|
}
|
||||||
"user": user,
|
|
||||||
"body": payload,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
r.raise_for_status()
|
try:
|
||||||
payload = r.json()
|
async with session.post(
|
||||||
except Exception as e:
|
f"{url}/{filter['id']}/filter/inlet",
|
||||||
# Handle connection error here
|
headers=headers,
|
||||||
print(f"Connection error: {e}")
|
json=request_data,
|
||||||
|
) as response:
|
||||||
if r is not None:
|
response.raise_for_status()
|
||||||
res = r.json()
|
payload = await response.json()
|
||||||
|
except aiohttp.ClientResponseError as e:
|
||||||
|
res = (
|
||||||
|
await response.json()
|
||||||
|
if response.content_type == "application/json"
|
||||||
|
else {}
|
||||||
|
)
|
||||||
if "detail" in res:
|
if "detail" in res:
|
||||||
raise Exception(r.status_code, res["detail"])
|
raise Exception(response.status, res["detail"])
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def process_pipeline_outlet_filter(request, payload, user, models):
|
async def process_pipeline_outlet_filter(request, payload, user, models):
|
||||||
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
|
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
|
||||||
model_id = payload["model"]
|
model_id = payload["model"]
|
||||||
|
|
||||||
sorted_filters = get_sorted_filters(model_id, models)
|
sorted_filters = get_sorted_filters(model_id, models)
|
||||||
model = models[model_id]
|
model = models[model_id]
|
||||||
|
|
||||||
if "pipeline" in model:
|
if "pipeline" in model:
|
||||||
sorted_filters = [model] + sorted_filters
|
sorted_filters = [model] + sorted_filters
|
||||||
|
|
||||||
for filter in sorted_filters:
|
async with aiohttp.ClientSession() as session:
|
||||||
r = None
|
for filter in sorted_filters:
|
||||||
try:
|
urlIdx = filter.get("urlIdx")
|
||||||
urlIdx = filter["urlIdx"]
|
if urlIdx is None:
|
||||||
|
continue
|
||||||
|
|
||||||
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||||
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
|
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||||
|
|
||||||
if key != "":
|
if not key:
|
||||||
r = requests.post(
|
continue
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
|
request_data = {
|
||||||
|
"user": user,
|
||||||
|
"body": payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with session.post(
|
||||||
f"{url}/{filter['id']}/filter/outlet",
|
f"{url}/{filter['id']}/filter/outlet",
|
||||||
headers={"Authorization": f"Bearer {key}"},
|
headers=headers,
|
||||||
json={
|
json=request_data,
|
||||||
"user": user,
|
) as response:
|
||||||
"body": payload,
|
response.raise_for_status()
|
||||||
},
|
payload = await response.json()
|
||||||
)
|
except aiohttp.ClientResponseError as e:
|
||||||
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
payload = data
|
|
||||||
except Exception as e:
|
|
||||||
# Handle connection error here
|
|
||||||
print(f"Connection error: {e}")
|
|
||||||
|
|
||||||
if r is not None:
|
|
||||||
try:
|
try:
|
||||||
res = r.json()
|
res = (
|
||||||
|
await response.json()
|
||||||
|
if "application/json" in response.content_type
|
||||||
|
else {}
|
||||||
|
)
|
||||||
if "detail" in res:
|
if "detail" in res:
|
||||||
return Exception(r.status_code, res)
|
raise Exception(response.status, res)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
except Exception as e:
|
||||||
else:
|
log.exception(f"Connection error: {e}")
|
||||||
pass
|
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@@ -161,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 = [
|
||||||
@@ -188,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(
|
||||||
@@ -223,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
|
||||||
@@ -274,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:
|
||||||
@@ -319,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:
|
||||||
@@ -353,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:
|
||||||
@@ -392,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:
|
||||||
@@ -432,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:
|
||||||
@@ -474,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
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from fastapi import (
|
|||||||
APIRouter,
|
APIRouter,
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import tiktoken
|
import tiktoken
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ from open_webui.retrieval.web.duckduckgo import search_duckduckgo
|
|||||||
from open_webui.retrieval.web.google_pse import search_google_pse
|
from open_webui.retrieval.web.google_pse import search_google_pse
|
||||||
from open_webui.retrieval.web.jina_search import search_jina
|
from open_webui.retrieval.web.jina_search import search_jina
|
||||||
from open_webui.retrieval.web.searchapi import search_searchapi
|
from open_webui.retrieval.web.searchapi import search_searchapi
|
||||||
|
from open_webui.retrieval.web.serpapi import search_serpapi
|
||||||
from open_webui.retrieval.web.searxng import search_searxng
|
from open_webui.retrieval.web.searxng import search_searxng
|
||||||
from open_webui.retrieval.web.serper import search_serper
|
from open_webui.retrieval.web.serper import search_serper
|
||||||
from open_webui.retrieval.web.serply import search_serply
|
from open_webui.retrieval.web.serply import search_serply
|
||||||
@@ -349,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,
|
||||||
@@ -369,10 +378,12 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
"proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
|
"proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"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,
|
||||||
|
"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,
|
||||||
@@ -388,11 +399,14 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
|
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
|
||||||
"searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
|
"searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
|
||||||
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
|
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
|
||||||
|
"serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
|
||||||
|
"serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
|
||||||
"jina_api_key": request.app.state.config.JINA_API_KEY,
|
"jina_api_key": request.app.state.config.JINA_API_KEY,
|
||||||
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||||
"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,
|
||||||
},
|
},
|
||||||
@@ -405,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):
|
||||||
@@ -439,23 +459,30 @@ class WebSearchConfig(BaseModel):
|
|||||||
tavily_api_key: Optional[str] = None
|
tavily_api_key: Optional[str] = None
|
||||||
searchapi_api_key: Optional[str] = None
|
searchapi_api_key: Optional[str] = None
|
||||||
searchapi_engine: Optional[str] = None
|
searchapi_engine: Optional[str] = None
|
||||||
|
serpapi_api_key: Optional[str] = None
|
||||||
|
serpapi_engine: Optional[str] = None
|
||||||
jina_api_key: Optional[str] = None
|
jina_api_key: Optional[str] = None
|
||||||
bing_search_v7_endpoint: Optional[str] = None
|
bing_search_v7_endpoint: Optional[str] = None
|
||||||
bing_search_v7_subscription_key: Optional[str] = None
|
bing_search_v7_subscription_key: Optional[str] = None
|
||||||
exa_api_key: Optional[str] = None
|
exa_api_key: Optional[str] = None
|
||||||
result_count: Optional[int] = None
|
result_count: Optional[int] = None
|
||||||
concurrent_requests: Optional[int] = None
|
concurrent_requests: Optional[int] = None
|
||||||
|
trust_env: Optional[bool] = None
|
||||||
domain_filter_list: Optional[List[str]] = []
|
domain_filter_list: Optional[List[str]] = []
|
||||||
|
|
||||||
|
|
||||||
class WebConfig(BaseModel):
|
class WebConfig(BaseModel):
|
||||||
search: WebSearchConfig
|
search: WebSearchConfig
|
||||||
web_loader_ssl_verification: Optional[bool] = None
|
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: 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
|
||||||
@@ -473,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
|
||||||
@@ -505,11 +559,16 @@ async def update_rag_config(
|
|||||||
if form_data.web is not None:
|
if form_data.web is not None:
|
||||||
request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
||||||
# Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False
|
# Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False
|
||||||
form_data.web.web_loader_ssl_verification
|
form_data.web.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
||||||
)
|
)
|
||||||
|
|
||||||
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.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
|
||||||
|
form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
|
)
|
||||||
|
|
||||||
request.app.state.config.SEARXNG_QUERY_URL = (
|
request.app.state.config.SEARXNG_QUERY_URL = (
|
||||||
form_data.web.search.searxng_query_url
|
form_data.web.search.searxng_query_url
|
||||||
)
|
)
|
||||||
@@ -545,6 +604,9 @@ async def update_rag_config(
|
|||||||
form_data.web.search.searchapi_engine
|
form_data.web.search.searchapi_engine
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.app.state.config.SERPAPI_API_KEY = form_data.web.search.serpapi_api_key
|
||||||
|
request.app.state.config.SERPAPI_ENGINE = form_data.web.search.serpapi_engine
|
||||||
|
|
||||||
request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key
|
request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key
|
||||||
request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
|
request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
|
||||||
form_data.web.search.bing_search_v7_endpoint
|
form_data.web.search.bing_search_v7_endpoint
|
||||||
@@ -561,6 +623,9 @@ async def update_rag_config(
|
|||||||
request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
||||||
form_data.web.search.concurrent_requests
|
form_data.web.search.concurrent_requests
|
||||||
)
|
)
|
||||||
|
request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV = (
|
||||||
|
form_data.web.search.trust_env
|
||||||
|
)
|
||||||
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = (
|
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = (
|
||||||
form_data.web.search.domain_filter_list
|
form_data.web.search.domain_filter_list
|
||||||
)
|
)
|
||||||
@@ -568,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,
|
||||||
@@ -575,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,
|
||||||
@@ -587,7 +658,8 @@ async def update_rag_config(
|
|||||||
"translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
|
"translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"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,
|
||||||
|
"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,
|
||||||
@@ -604,6 +676,8 @@ async def update_rag_config(
|
|||||||
"serply_api_key": request.app.state.config.SERPLY_API_KEY,
|
"serply_api_key": request.app.state.config.SERPLY_API_KEY,
|
||||||
"serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
|
"serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
|
||||||
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
|
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
|
||||||
|
"serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
|
||||||
|
"serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
|
||||||
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
|
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
|
||||||
"jina_api_key": request.app.state.config.JINA_API_KEY,
|
"jina_api_key": request.app.state.config.JINA_API_KEY,
|
||||||
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||||
@@ -611,6 +685,7 @@ async def update_rag_config(
|
|||||||
"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,
|
||||||
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
|
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
||||||
"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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -760,7 +835,11 @@ def save_docs_to_vector_db(
|
|||||||
# for meta-data so convert them to string.
|
# for meta-data so convert them to string.
|
||||||
for metadata in metadatas:
|
for metadata in metadatas:
|
||||||
for key, value in metadata.items():
|
for key, value in metadata.items():
|
||||||
if isinstance(value, datetime):
|
if (
|
||||||
|
isinstance(value, datetime)
|
||||||
|
or isinstance(value, list)
|
||||||
|
or isinstance(value, dict)
|
||||||
|
):
|
||||||
metadata[key] = str(value)
|
metadata[key] = str(value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -843,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(
|
||||||
@@ -900,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
|
||||||
@@ -942,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):
|
||||||
@@ -1127,6 +1222,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
|||||||
- TAVILY_API_KEY
|
- TAVILY_API_KEY
|
||||||
- EXA_API_KEY
|
- EXA_API_KEY
|
||||||
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
|
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
|
||||||
|
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
|
||||||
Args:
|
Args:
|
||||||
query (str): The query to search for
|
query (str): The query to search for
|
||||||
"""
|
"""
|
||||||
@@ -1241,6 +1337,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
|||||||
request.app.state.config.TAVILY_API_KEY,
|
request.app.state.config.TAVILY_API_KEY,
|
||||||
query,
|
query,
|
||||||
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
|
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("No TAVILY_API_KEY found in environment variables")
|
raise Exception("No TAVILY_API_KEY found in environment variables")
|
||||||
@@ -1255,6 +1352,17 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("No SEARCHAPI_API_KEY found in environment variables")
|
raise Exception("No SEARCHAPI_API_KEY found in environment variables")
|
||||||
|
elif engine == "serpapi":
|
||||||
|
if request.app.state.config.SERPAPI_API_KEY:
|
||||||
|
return search_serpapi(
|
||||||
|
request.app.state.config.SERPAPI_API_KEY,
|
||||||
|
request.app.state.config.SERPAPI_ENGINE,
|
||||||
|
query,
|
||||||
|
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
|
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception("No SERPAPI_API_KEY found in environment variables")
|
||||||
elif engine == "jina":
|
elif engine == "jina":
|
||||||
return search_jina(
|
return search_jina(
|
||||||
request.app.state.config.JINA_API_KEY,
|
request.app.state.config.JINA_API_KEY,
|
||||||
@@ -1282,7 +1390,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/process/web/search")
|
@router.post("/process/web/search")
|
||||||
def process_web_search(
|
async def process_web_search(
|
||||||
request: Request, form_data: SearchForm, user=Depends(get_verified_user)
|
request: Request, form_data: SearchForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
@@ -1314,17 +1422,40 @@ def process_web_search(
|
|||||||
urls,
|
urls,
|
||||||
verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||||
requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
|
trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
||||||
)
|
)
|
||||||
docs = loader.load()
|
docs = await loader.aload()
|
||||||
save_docs_to_vector_db(
|
|
||||||
request, docs, collection_name, overwrite=True, user=user
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
|
||||||
"status": True,
|
return {
|
||||||
"collection_name": collection_name,
|
"status": True,
|
||||||
"filenames": urls,
|
"collection_name": None,
|
||||||
}
|
"filenames": urls,
|
||||||
|
"docs": [
|
||||||
|
{
|
||||||
|
"content": doc.page_content,
|
||||||
|
"metadata": doc.metadata,
|
||||||
|
}
|
||||||
|
for doc in docs
|
||||||
|
],
|
||||||
|
"loaded_count": len(docs),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
await run_in_threadpool(
|
||||||
|
save_docs_to_vector_db,
|
||||||
|
request,
|
||||||
|
docs,
|
||||||
|
collection_name,
|
||||||
|
overwrite=True,
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"collection_name": collection_name,
|
||||||
|
"filenames": urls,
|
||||||
|
"loaded_count": len(docs),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -1481,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -58,6 +62,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
|
|||||||
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
||||||
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||||
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
|
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
|
||||||
|
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
|
||||||
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
|
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
|
||||||
"ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
|
"ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
|
||||||
"QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
|
"QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||||
@@ -68,6 +73,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
|
|||||||
class TaskConfigForm(BaseModel):
|
class TaskConfigForm(BaseModel):
|
||||||
TASK_MODEL: Optional[str]
|
TASK_MODEL: Optional[str]
|
||||||
TASK_MODEL_EXTERNAL: Optional[str]
|
TASK_MODEL_EXTERNAL: Optional[str]
|
||||||
|
ENABLE_TITLE_GENERATION: bool
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||||
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
|
||||||
ENABLE_AUTOCOMPLETE_GENERATION: bool
|
ENABLE_AUTOCOMPLETE_GENERATION: bool
|
||||||
@@ -86,6 +92,7 @@ async def update_task_config(
|
|||||||
):
|
):
|
||||||
request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
|
request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
|
||||||
request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
|
request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
|
||||||
|
request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION
|
||||||
request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
||||||
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||||
)
|
)
|
||||||
@@ -122,6 +129,7 @@ async def update_task_config(
|
|||||||
return {
|
return {
|
||||||
"TASK_MODEL": request.app.state.config.TASK_MODEL,
|
"TASK_MODEL": request.app.state.config.TASK_MODEL,
|
||||||
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
|
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
|
||||||
|
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
|
||||||
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||||
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||||
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
||||||
@@ -139,6 +147,13 @@ async def update_task_config(
|
|||||||
async def generate_title(
|
async def generate_title(
|
||||||
request: Request, form_data: dict, user=Depends(get_verified_user)
|
request: Request, form_data: dict, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
|
if not request.app.state.config.ENABLE_TITLE_GENERATION:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
content={"detail": "Title generation is disabled"},
|
||||||
|
)
|
||||||
|
|
||||||
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
||||||
models = {
|
models = {
|
||||||
request.state.model["id"]: request.state.model,
|
request.state.model["id"]: request.state.model,
|
||||||
@@ -197,7 +212,7 @@ async def generate_title(
|
|||||||
"stream": False,
|
"stream": False,
|
||||||
**(
|
**(
|
||||||
{"max_tokens": 1000}
|
{"max_tokens": 1000}
|
||||||
if models[task_model_id]["owned_by"] == "ollama"
|
if models[task_model_id].get("owned_by") == "ollama"
|
||||||
else {
|
else {
|
||||||
"max_completion_tokens": 1000,
|
"max_completion_tokens": 1000,
|
||||||
}
|
}
|
||||||
@@ -210,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:
|
||||||
@@ -279,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:
|
||||||
@@ -345,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:
|
||||||
@@ -422,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:
|
||||||
@@ -503,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:
|
||||||
@@ -560,7 +605,7 @@ async def generate_emoji(
|
|||||||
"stream": False,
|
"stream": False,
|
||||||
**(
|
**(
|
||||||
{"max_tokens": 4}
|
{"max_tokens": 4}
|
||||||
if models[task_model_id]["owned_by"] == "ollama"
|
if models[task_model_id].get("owned_by") == "ollama"
|
||||||
else {
|
else {
|
||||||
"max_completion_tokens": 4,
|
"max_completion_tokens": 4,
|
||||||
}
|
}
|
||||||
@@ -573,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:
|
||||||
@@ -633,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:
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -1,48 +1,84 @@
|
|||||||
import black
|
import black
|
||||||
|
import logging
|
||||||
import markdown
|
import markdown
|
||||||
|
|
||||||
from open_webui.models.chats import ChatTitleMessagesForm
|
from open_webui.models.chats import ChatTitleMessagesForm
|
||||||
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
|
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.misc import get_gravatar_url
|
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
|
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.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/gravatar")
|
@router.get("/gravatar")
|
||||||
async def get_gravatar(
|
async def get_gravatar(email: str, user=Depends(get_verified_user)):
|
||||||
email: str,
|
|
||||||
):
|
|
||||||
return get_gravatar_url(email)
|
return get_gravatar_url(email)
|
||||||
|
|
||||||
|
|
||||||
class CodeFormatRequest(BaseModel):
|
class CodeForm(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/code/format")
|
@router.post("/code/format")
|
||||||
async def format_code(request: CodeFormatRequest):
|
async def format_code(form_data: CodeForm, user=Depends(get_verified_user)):
|
||||||
try:
|
try:
|
||||||
formatted_code = black.format_str(request.code, mode=black.Mode())
|
formatted_code = black.format_str(form_data.code, mode=black.Mode())
|
||||||
return {"code": formatted_code}
|
return {"code": formatted_code}
|
||||||
except black.NothingChanged:
|
except black.NothingChanged:
|
||||||
return {"code": request.code}
|
return {"code": form_data.code}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/code/execute")
|
||||||
|
async def execute_code(
|
||||||
|
request: Request, form_data: CodeForm, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
|
if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter":
|
||||||
|
output = await execute_code_jupyter(
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||||
|
form_data.code,
|
||||||
|
(
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
|
||||||
|
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token"
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
(
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
|
||||||
|
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password"
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Code execution engine not supported",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MarkdownForm(BaseModel):
|
class MarkdownForm(BaseModel):
|
||||||
md: str
|
md: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/markdown")
|
@router.post("/markdown")
|
||||||
async def get_html_from_markdown(
|
async def get_html_from_markdown(
|
||||||
form_data: MarkdownForm,
|
form_data: MarkdownForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
return {"html": markdown.markdown(form_data.md)}
|
return {"html": markdown.markdown(form_data.md)}
|
||||||
|
|
||||||
@@ -54,7 +90,7 @@ class ChatForm(BaseModel):
|
|||||||
|
|
||||||
@router.post("/pdf")
|
@router.post("/pdf")
|
||||||
async def download_chat_as_pdf(
|
async def download_chat_as_pdf(
|
||||||
form_data: ChatTitleMessagesForm,
|
form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()
|
pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()
|
||||||
@@ -65,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))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9308,5 +9308,3 @@
|
|||||||
.json-schema-2020-12__title:first-of-type {
|
.json-schema-2020-12__title:first-of-type {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=swagger-ui.css.map*/
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)}")
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
import jwt
|
import jwt
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Optional, Union, List, Dict
|
from typing import Optional, Union, List, Dict
|
||||||
@@ -8,14 +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.env import WEBUI_SECRET_KEY
|
from open_webui.env import (
|
||||||
|
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"
|
||||||
@@ -24,6 +38,66 @@ ALGORITHM = "HS256"
|
|||||||
# Auth Utils
|
# Auth Utils
|
||||||
##############
|
##############
|
||||||
|
|
||||||
|
|
||||||
|
def verify_signature(payload: str, signature: str) -> bool:
|
||||||
|
"""
|
||||||
|
Verifies the HMAC signature of the received payload.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
expected_signature = base64.b64encode(
|
||||||
|
hmac.new(TRUSTED_SIGNATURE_KEY, payload.encode(), hashlib.sha256).digest()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
# Compare securely to prevent timing attacks
|
||||||
|
return hmac.compare_digest(expected_signature, signature)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
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):
|
||||||
|
if key:
|
||||||
|
try:
|
||||||
|
res = requests.post(
|
||||||
|
"https://api.openwebui.com/api/v1/license",
|
||||||
|
json={"key": key, "version": "1"},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
if getattr(res, "ok", False):
|
||||||
|
payload = getattr(res, "json", lambda: {})()
|
||||||
|
for k, v in payload.items():
|
||||||
|
if k == "resources":
|
||||||
|
for p, c in v.items():
|
||||||
|
globals().get("override_static", lambda a, b: None)(p, c)
|
||||||
|
elif k == "user_count":
|
||||||
|
setattr(app.state, "USER_COUNT", v)
|
||||||
|
elif k == "webui_name":
|
||||||
|
setattr(app.state, "WEBUI_NAME", v)
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
log.exception(f"License: Uncaught Exception: {ex}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
bearer_security = HTTPBearer(auto_error=False)
|
bearer_security = HTTPBearer(auto_error=False)
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
@@ -76,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
|
||||||
@@ -128,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(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -161,11 +161,18 @@ async def generate_chat_completion(
|
|||||||
user: Any,
|
user: Any,
|
||||||
bypass_filter: bool = False,
|
bypass_filter: bool = False,
|
||||||
):
|
):
|
||||||
|
log.debug(f"generate_chat_completion: {form_data}")
|
||||||
if BYPASS_MODEL_ACCESS_CONTROL:
|
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||||
bypass_filter = True
|
bypass_filter = True
|
||||||
|
|
||||||
if hasattr(request.state, "metadata"):
|
if hasattr(request.state, "metadata"):
|
||||||
form_data["metadata"] = request.state.metadata
|
if "metadata" not in form_data:
|
||||||
|
form_data["metadata"] = request.state.metadata
|
||||||
|
else:
|
||||||
|
form_data["metadata"] = {
|
||||||
|
**form_data["metadata"],
|
||||||
|
**request.state.metadata,
|
||||||
|
}
|
||||||
|
|
||||||
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
||||||
models = {
|
models = {
|
||||||
@@ -179,28 +186,21 @@ async def generate_chat_completion(
|
|||||||
if model_id not in models:
|
if model_id not in models:
|
||||||
raise Exception("Model not found")
|
raise Exception("Model not found")
|
||||||
|
|
||||||
# Process the form_data through the pipeline
|
|
||||||
try:
|
|
||||||
form_data = process_pipeline_inlet_filter(request, form_data, user, models)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
model = models[model_id]
|
model = models[model_id]
|
||||||
|
|
||||||
# Check if user has access to the model
|
|
||||||
if not bypass_filter and user.role == "user":
|
|
||||||
try:
|
|
||||||
check_model_access(user, model)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if getattr(request.state, "direct", False):
|
if getattr(request.state, "direct", False):
|
||||||
return await generate_direct_chat_completion(
|
return await generate_direct_chat_completion(
|
||||||
request, form_data, user=user, models=models
|
request, form_data, user=user, models=models
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if model["owned_by"] == "arena":
|
# Check if user has access to the model
|
||||||
|
if not bypass_filter and user.role == "user":
|
||||||
|
try:
|
||||||
|
check_model_access(user, model)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if model.get("owned_by") == "arena":
|
||||||
model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
|
model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
|
||||||
filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
|
filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
|
||||||
if model_ids and filter_mode == "exclude":
|
if model_ids and filter_mode == "exclude":
|
||||||
@@ -253,7 +253,7 @@ async def generate_chat_completion(
|
|||||||
return await generate_function_chat_completion(
|
return await generate_function_chat_completion(
|
||||||
request, form_data, user=user, models=models
|
request, form_data, user=user, models=models
|
||||||
)
|
)
|
||||||
if model["owned_by"] == "ollama":
|
if model.get("owned_by") == "ollama":
|
||||||
# Using /ollama/api/chat endpoint
|
# Using /ollama/api/chat endpoint
|
||||||
form_data = convert_payload_openai_to_ollama(form_data)
|
form_data = convert_payload_openai_to_ollama(form_data)
|
||||||
response = await generate_ollama_chat_completion(
|
response = await generate_ollama_chat_completion(
|
||||||
@@ -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 = {
|
||||||
@@ -302,7 +302,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
|
|||||||
model = models[model_id]
|
model = models[model_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = process_pipeline_outlet_filter(request, data, user, models)
|
data = await process_pipeline_outlet_filter(request, data, user, models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Exception(f"Error: {e}")
|
return Exception(f"Error: {e}")
|
||||||
|
|
||||||
@@ -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__}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -39,7 +39,10 @@ from open_webui.routers.tasks import (
|
|||||||
)
|
)
|
||||||
from open_webui.routers.retrieval import process_web_search, SearchForm
|
from open_webui.routers.retrieval import process_web_search, SearchForm
|
||||||
from open_webui.routers.images import image_generations, GenerateImageForm
|
from open_webui.routers.images import image_generations, GenerateImageForm
|
||||||
|
from open_webui.routers.pipelines import (
|
||||||
|
process_pipeline_inlet_filter,
|
||||||
|
process_pipeline_outlet_filter,
|
||||||
|
)
|
||||||
|
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
|
|
||||||
@@ -318,84 +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,
|
"query": searchQuery,
|
||||||
},
|
"done": False,
|
||||||
}
|
},
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
results = await process_web_search(
|
||||||
# Offload process_web_search to a separate thread
|
request,
|
||||||
loop = asyncio.get_running_loop()
|
SearchForm(
|
||||||
with ThreadPoolExecutor() as executor:
|
**{
|
||||||
results = await loop.run_in_executor(
|
|
||||||
executor,
|
|
||||||
lambda: process_web_search(
|
|
||||||
request,
|
|
||||||
SearchForm(
|
|
||||||
**{
|
|
||||||
"query": searchQuery,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if results:
|
|
||||||
await event_emitter(
|
|
||||||
{
|
|
||||||
"type": "status",
|
|
||||||
"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:
|
||||||
files.append(
|
all_results.append(results)
|
||||||
{
|
files = form_data.get("files", [])
|
||||||
"collection_name": results["collection_name"],
|
|
||||||
"name": searchQuery,
|
if results.get("collection_name"):
|
||||||
"type": "web_search_results",
|
files.append(
|
||||||
"urls": results["filenames"],
|
{
|
||||||
}
|
"collection_name": results["collection_name"],
|
||||||
)
|
"name": searchQuery,
|
||||||
form_data["files"] = files
|
"type": "web_search",
|
||||||
else:
|
"urls": results["filenames"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif results.get("docs"):
|
||||||
|
files.append(
|
||||||
|
{
|
||||||
|
"docs": results.get("docs", []),
|
||||||
|
"name": searchQuery,
|
||||||
|
"type": "web_search",
|
||||||
|
"urls": results["filenames"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
form_data["files"] = files
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
await event_emitter(
|
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,
|
||||||
},
|
},
|
||||||
@@ -505,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,
|
||||||
@@ -530,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"])]
|
||||||
@@ -543,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(
|
||||||
@@ -552,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)
|
||||||
|
|
||||||
@@ -682,6 +698,25 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
|
|
||||||
variables = form_data.pop("variables", None)
|
variables = form_data.pop("variables", None)
|
||||||
|
|
||||||
|
# Process the form_data through the pipeline
|
||||||
|
try:
|
||||||
|
form_data = await process_pipeline_inlet_filter(
|
||||||
|
request, form_data, user, models
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
try:
|
||||||
|
form_data, flags = await process_filter_functions(
|
||||||
|
request=request,
|
||||||
|
filter_ids=get_sorted_filter_ids(model),
|
||||||
|
filter_type="inlet",
|
||||||
|
form_data=form_data,
|
||||||
|
extra_params=extra_params,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Error: {e}")
|
||||||
|
|
||||||
features = form_data.pop("features", None)
|
features = form_data.pop("features", None)
|
||||||
if features:
|
if features:
|
||||||
if "web_search" in features and features["web_search"]:
|
if "web_search" in features and features["web_search"]:
|
||||||
@@ -704,19 +739,9 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
form_data["messages"],
|
form_data["messages"],
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
form_data, flags = await process_filter_functions(
|
|
||||||
request=request,
|
|
||||||
filter_ids=get_sorted_filter_ids(model),
|
|
||||||
filter_type="inlet",
|
|
||||||
form_data=form_data,
|
|
||||||
extra_params=extra_params,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"Error: {e}")
|
|
||||||
|
|
||||||
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())
|
||||||
@@ -774,11 +799,9 @@ 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>{doc_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"
|
||||||
|
|
||||||
context_string = context_string.strip()
|
context_string = context_string.strip()
|
||||||
prompt = get_last_user_message(form_data["messages"])
|
prompt = get_last_user_message(form_data["messages"])
|
||||||
@@ -795,7 +818,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
|
|
||||||
# Workaround for Ollama 2.0+ system prompt issue
|
# Workaround for Ollama 2.0+ system prompt issue
|
||||||
# TODO: replace with add_or_update_system_message
|
# TODO: replace with add_or_update_system_message
|
||||||
if model["owned_by"] == "ollama":
|
if model.get("owned_by") == "ollama":
|
||||||
form_data["messages"] = prepend_to_first_user_message_content(
|
form_data["messages"] = prepend_to_first_user_message_content(
|
||||||
rag_template(
|
rag_template(
|
||||||
request.app.state.config.RAG_TEMPLATE, context_string, prompt
|
request.app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||||
@@ -1003,6 +1026,7 @@ async def process_chat_response(
|
|||||||
webhook_url = Users.get_user_webhook_url_by_id(user.id)
|
webhook_url = Users.get_user_webhook_url_by_id(user.id)
|
||||||
if webhook_url:
|
if webhook_url:
|
||||||
post_webhook(
|
post_webhook(
|
||||||
|
request.app.state.WEBUI_NAME,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
|
f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
|
||||||
{
|
{
|
||||||
@@ -1026,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.
|
||||||
@@ -1105,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'
|
||||||
|
|
||||||
@@ -1151,6 +1190,46 @@ async def process_chat_response(
|
|||||||
|
|
||||||
return content.strip()
|
return content.strip()
|
||||||
|
|
||||||
|
def convert_content_blocks_to_messages(content_blocks):
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
temp_blocks = []
|
||||||
|
for idx, block in enumerate(content_blocks):
|
||||||
|
if block["type"] == "tool_calls":
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": serialize_content_blocks(temp_blocks),
|
||||||
|
"tool_calls": block.get("content"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
results = block.get("results", [])
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": result["tool_call_id"],
|
||||||
|
"content": result["content"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
temp_blocks = []
|
||||||
|
else:
|
||||||
|
temp_blocks.append(block)
|
||||||
|
|
||||||
|
if temp_blocks:
|
||||||
|
content = serialize_content_blocks(temp_blocks)
|
||||||
|
if content:
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
def tag_content_handler(content_type, tags, content, content_blocks):
|
def tag_content_handler(content_type, tags, content, content_blocks):
|
||||||
end_flag = False
|
end_flag = False
|
||||||
|
|
||||||
@@ -1166,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 = (
|
||||||
@@ -1201,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(),
|
||||||
@@ -1213,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):
|
||||||
@@ -1223,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()
|
||||||
@@ -1288,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,
|
||||||
@@ -1301,7 +1382,22 @@ async def process_chat_response(
|
|||||||
)
|
)
|
||||||
|
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
content = message.get("content", "") if message else ""
|
|
||||||
|
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 = (
|
||||||
|
message.get("content", "")
|
||||||
|
if message
|
||||||
|
else last_assistant_message if last_assistant_message else ""
|
||||||
|
)
|
||||||
|
|
||||||
content_blocks = [
|
content_blocks = [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1311,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:
|
||||||
@@ -1367,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:
|
||||||
@@ -1542,7 +1678,6 @@ async def process_chat_response(
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for tool_call in response_tool_calls:
|
for tool_call in response_tool_calls:
|
||||||
print("\n\n" + str(tool_call) + "\n\n")
|
|
||||||
tool_call_id = tool_call.get("id", "")
|
tool_call_id = tool_call.get("id", "")
|
||||||
tool_name = tool_call.get("function", {}).get("name", "")
|
tool_name = tool_call.get("function", {}).get("name", "")
|
||||||
|
|
||||||
@@ -1608,23 +1743,10 @@ async def process_chat_response(
|
|||||||
{
|
{
|
||||||
"model": model_id,
|
"model": model_id,
|
||||||
"stream": True,
|
"stream": True,
|
||||||
|
"tools": form_data["tools"],
|
||||||
"messages": [
|
"messages": [
|
||||||
*form_data["messages"],
|
*form_data["messages"],
|
||||||
{
|
*convert_content_blocks_to_messages(content_blocks),
|
||||||
"role": "assistant",
|
|
||||||
"content": serialize_content_blocks(
|
|
||||||
content_blocks, raw=True
|
|
||||||
),
|
|
||||||
"tool_calls": response_tool_calls,
|
|
||||||
},
|
|
||||||
*[
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": result["tool_call_id"],
|
|
||||||
"content": result["content"],
|
|
||||||
}
|
|
||||||
for result in results
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
user,
|
user,
|
||||||
@@ -1698,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 = {
|
||||||
@@ -1791,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(
|
||||||
@@ -1842,6 +1968,7 @@ async def process_chat_response(
|
|||||||
webhook_url = Users.get_user_webhook_url_by_id(user.id)
|
webhook_url = Users.get_user_webhook_url_by_id(user.id)
|
||||||
if webhook_url:
|
if webhook_url:
|
||||||
post_webhook(
|
post_webhook(
|
||||||
|
request.app.state.WEBUI_NAME,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
|
f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
|
||||||
{
|
{
|
||||||
@@ -1861,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:
|
||||||
@@ -1882,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),
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -225,10 +230,11 @@ def openai_chat_completion_message_template(
|
|||||||
template = openai_chat_message_template(model)
|
template = openai_chat_message_template(model)
|
||||||
template["object"] = "chat.completion"
|
template["object"] = "chat.completion"
|
||||||
if message is not None:
|
if message is not None:
|
||||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
template["choices"][0]["message"] = {
|
||||||
|
"content": message,
|
||||||
if tool_calls:
|
"role": "assistant",
|
||||||
template["choices"][0]["tool_calls"] = tool_calls
|
**({"tool_calls": tool_calls} if tool_calls else {}),
|
||||||
|
}
|
||||||
|
|
||||||
template["choices"][0]["finish_reason"] = "stop"
|
template["choices"][0]["finish_reason"] = "stop"
|
||||||
|
|
||||||
@@ -411,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
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -142,7 +143,7 @@ async def get_all_models(request):
|
|||||||
custom_model.base_model_id == model["id"]
|
custom_model.base_model_id == model["id"]
|
||||||
or custom_model.base_model_id == model["id"].split(":")[0]
|
or custom_model.base_model_id == model["id"].split(":")[0]
|
||||||
):
|
):
|
||||||
owned_by = model["owned_by"]
|
owned_by = model.get("owned_by", "unknown owner")
|
||||||
if "pipe" in model:
|
if "pipe" in model:
|
||||||
pipe = model["pipe"]
|
pipe = model["pipe"]
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ from open_webui.config import (
|
|||||||
AppConfig,
|
AppConfig,
|
||||||
)
|
)
|
||||||
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||||
from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE
|
from open_webui.env import (
|
||||||
|
WEBUI_NAME,
|
||||||
|
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
|
WEBUI_AUTH_COOKIE_SECURE,
|
||||||
|
)
|
||||||
from open_webui.utils.misc import parse_duration
|
from open_webui.utils.misc import parse_duration
|
||||||
from open_webui.utils.auth import get_password_hash, create_token
|
from open_webui.utils.auth import get_password_hash, create_token
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
@@ -66,8 +70,9 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
|||||||
|
|
||||||
|
|
||||||
class OAuthManager:
|
class OAuthManager:
|
||||||
def __init__(self):
|
def __init__(self, app):
|
||||||
self.oauth = OAuth()
|
self.oauth = OAuth()
|
||||||
|
self.app = app
|
||||||
for _, provider_config in OAUTH_PROVIDERS.items():
|
for _, provider_config in OAUTH_PROVIDERS.items():
|
||||||
provider_config["register"](self.oauth)
|
provider_config["register"](self.oauth)
|
||||||
|
|
||||||
@@ -135,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()
|
||||||
|
|
||||||
@@ -200,7 +212,7 @@ class OAuthManager:
|
|||||||
id=group_model.id, form_data=update_form, overwrite=False
|
id=group_model.id, form_data=update_form, overwrite=False
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_login(self, provider, request):
|
async def handle_login(self, request, provider):
|
||||||
if provider not in OAUTH_PROVIDERS:
|
if provider not in OAUTH_PROVIDERS:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
|
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
|
||||||
@@ -212,7 +224,7 @@ class OAuthManager:
|
|||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
return await client.authorize_redirect(request, redirect_uri)
|
return await client.authorize_redirect(request, redirect_uri)
|
||||||
|
|
||||||
async def handle_callback(self, provider, request, response):
|
async def handle_callback(self, request, provider, response):
|
||||||
if provider not in OAUTH_PROVIDERS:
|
if provider not in OAUTH_PROVIDERS:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
client = self.get_client(provider)
|
client = self.get_client(provider)
|
||||||
@@ -234,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
|
||||||
@@ -266,12 +313,12 @@ class OAuthManager:
|
|||||||
Users.update_user_role_by_id(user.id, determined_role)
|
Users.update_user_role_by_id(user.id, determined_role)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
|
user_count = Users.get_num_users()
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
@@ -334,6 +381,7 @@ class OAuthManager:
|
|||||||
|
|
||||||
if auth_manager_config.WEBHOOK_URL:
|
if auth_manager_config.WEBHOOK_URL:
|
||||||
post_webhook(
|
post_webhook(
|
||||||
|
WEBUI_NAME,
|
||||||
auth_manager_config.WEBHOOK_URL,
|
auth_manager_config.WEBHOOK_URL,
|
||||||
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||||
{
|
{
|
||||||
@@ -380,6 +428,3 @@ class OAuthManager:
|
|||||||
# Redirect back to the frontend with the JWT token
|
# Redirect back to the frontend with the JWT token
|
||||||
redirect_url = f"{request.base_url}auth#token={jwt_token}"
|
redirect_url = f"{request.base_url}auth#token={jwt_token}"
|
||||||
return RedirectResponse(url=redirect_url, headers=response.headers)
|
return RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
|
||||||
|
|
||||||
oauth_manager = OAuthManager()
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def get_task_model_id(
|
|||||||
# Set the task model
|
# Set the task model
|
||||||
task_model_id = default_model_id
|
task_model_id = default_model_id
|
||||||
# Check if the user has a custom task model and use that model
|
# Check if the user has a custom task model and use that model
|
||||||
if models[task_model_id]["owned_by"] == "ollama":
|
if models[task_model_id].get("owned_by") == "ollama":
|
||||||
if task_model and task_model in models:
|
if task_model and task_model in models:
|
||||||
task_model_id = task_model
|
task_model_id = task_model
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from open_webui.config import WEBUI_FAVICON_URL, WEBUI_NAME
|
from open_webui.config import WEBUI_FAVICON_URL
|
||||||
from open_webui.env import SRC_LOG_LEVELS, VERSION
|
from open_webui.env import SRC_LOG_LEVELS, VERSION
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
|
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
|
||||||
|
|
||||||
|
|
||||||
def post_webhook(url: str, message: str, event_data: dict) -> bool:
|
def post_webhook(name: str, url: str, message: str, event_data: dict) -> bool:
|
||||||
try:
|
try:
|
||||||
log.debug(f"post_webhook: {url}, {message}, {event_data}")
|
log.debug(f"post_webhook: {url}, {message}, {event_data}")
|
||||||
payload = {}
|
payload = {}
|
||||||
@@ -39,7 +39,7 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool:
|
|||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"activityTitle": message,
|
"activityTitle": message,
|
||||||
"activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}",
|
"activitySubtitle": f"{name} ({VERSION}) - {action}",
|
||||||
"activityImage": WEBUI_FAVICON_URL,
|
"activityImage": WEBUI_FAVICON_URL,
|
||||||
"facts": facts,
|
"facts": facts,
|
||||||
"markdown": True,
|
"markdown": True,
|
||||||
|
|||||||
@@ -1,13 +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
|
||||||
|
|
||||||
Flask==3.1.0
|
|
||||||
Flask-Cors==5.0.0
|
|
||||||
|
|
||||||
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
|
||||||
@@ -34,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
|
||||||
@@ -48,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
|
||||||
@@ -62,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
|
||||||
@@ -74,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
|
||||||
@@ -106,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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
Generated
+731
-525
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.11",
|
"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",
|
||||||
@@ -26,10 +26,10 @@
|
|||||||
"@sveltejs/kit": "^2.5.20",
|
"@sveltejs/kit": "^2.5.20",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/parser": "^6.17.0",
|
||||||
"autoprefixer": "^10.4.16",
|
|
||||||
"cypress": "^13.15.0",
|
"cypress": "^13.15.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
"svelte-check": "^3.8.5",
|
"svelte-check": "^3.8.5",
|
||||||
"svelte-confetti": "^1.3.2",
|
"svelte-confetti": "^1.3.2",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^4.0.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.14",
|
"vite": "^5.4.14",
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {}
|
||||||
autoprefixer: {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-6
@@ -8,14 +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",
|
||||||
|
|
||||||
"Flask==3.1.0",
|
|
||||||
"Flask-Cors==5.0.0",
|
|
||||||
|
|
||||||
"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",
|
||||||
@@ -43,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",
|
||||||
@@ -56,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",
|
||||||
@@ -68,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",
|
||||||
@@ -80,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",
|
||||||
@@ -111,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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
+17
-5
@@ -1,3 +1,5 @@
|
|||||||
|
@reference "./tailwind.css";
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
src: url('/assets/fonts/Inter-Variable.ttf');
|
src: url('/assets/fonts/Inter-Variable.ttf');
|
||||||
@@ -53,11 +55,11 @@ math {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-prose {
|
.markdown-prose {
|
||||||
@apply prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
@apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-prose-xs {
|
.markdown-prose-xs {
|
||||||
@apply text-xs prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
@apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown a {
|
.markdown a {
|
||||||
@@ -99,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +219,18 @@ input[type='number'] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
.cm-scroller:active::-webkit-scrollbar-thumb,
|
||||||
@apply scrollbar-hidden;
|
.cm-scroller:focus::-webkit-scrollbar-thumb,
|
||||||
|
.cm-scroller:hover::-webkit-scrollbar-thumb {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-scroller::-webkit-scrollbar-thumb {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-scroller::-webkit-scrollbar-corner {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor.cm-focused {
|
.cm-editor.cm-focused {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
title="Open WebUI"
|
title="Open WebUI"
|
||||||
href="/opensearch.xml"
|
href="/opensearch.xml"
|
||||||
/>
|
/>
|
||||||
|
<script src="/static/loader.js" defer></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function resizeIframe(obj) {
|
function resizeIframe(obj) {
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ export const getChatById = async (token: string, id: string) => {
|
|||||||
return json;
|
return json;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
error = err;
|
error = err.detail;
|
||||||
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCodeInterpreterConfig = async (token: string) => {
|
export const getCodeExecutionConfig = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -142,10 +142,10 @@ export const getCodeInterpreterConfig = async (token: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setCodeInterpreterConfig = async (token: string, config: object) => {
|
export const setCodeExecutionConfig = async (token: string, config: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -284,14 +284,16 @@ export const updateUserInfo = async (token: string, info: object) => {
|
|||||||
|
|
||||||
export const getAndUpdateUserLocation = async (token: string) => {
|
export const getAndUpdateUserLocation = async (token: string) => {
|
||||||
const location = await getUserPosition().catch((err) => {
|
const location = await getUserPosition().catch((err) => {
|
||||||
throw err;
|
console.log(err);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (location) {
|
if (location) {
|
||||||
await updateUserInfo(token, { location: location });
|
await updateUserInfo(token, { location: location });
|
||||||
return location;
|
return location;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to get user location');
|
console.log('Failed to get user location');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getGravatarUrl = async (email: string) => {
|
export const getGravatarUrl = async (token: string, email: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -22,13 +23,14 @@ export const getGravatarUrl = async (email: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatPythonCode = async (code: string) => {
|
export const executeCode = async (token: string, code: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/execute`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
code: code
|
code: code
|
||||||
@@ -55,13 +57,48 @@ export const formatPythonCode = async (code: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const downloadChatAsPDF = async (title: string, messages: object[]) => {
|
export const formatPythonCode = async (token: string, code: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: code
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
error = err;
|
||||||
|
if (err.detail) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const downloadChatAsPDF = async (token: string, title: string, messages: object[]) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
|
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: title,
|
title: title,
|
||||||
@@ -81,13 +118,14 @@ export const downloadChatAsPDF = async (title: string, messages: object[]) => {
|
|||||||
return blob;
|
return blob;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHTMLFromMarkdown = async (md: string) => {
|
export const getHTMLFromMarkdown = async (token: string, md: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
md: md
|
md: md
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div class="flex flex-col flex-shrink-0 self-end">
|
<div class="flex flex-col shrink-0 self-end">
|
||||||
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||||
<Switch bind:state={enable} />
|
<Switch bind:state={enable} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
bind:value={key}
|
bind:value={key}
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
required={!ollama}
|
required={!ollama}
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={prefixId}
|
bind:value={prefixId}
|
||||||
placeholder={$i18n.t('Prefix ID')}
|
placeholder={$i18n.t('Prefix ID')}
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||||
{modelId}
|
{modelId}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
<input
|
<input
|
||||||
class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
|
class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
bind:value={modelId}
|
bind:value={modelId}
|
||||||
placeholder={$i18n.t('Add a model ID')}
|
placeholder={$i18n.t('Add a model ID')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
v{version} - {changelog[version].date}
|
v{version} - {changelog[version].date}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800 my-2" />
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-50 dark:border-gray-800 rounded-xl px-3.5 py-3.5"
|
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-850 rounded-xl px-3.5 py-3.5"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
onClick();
|
onClick();
|
||||||
dispatch('closeToast');
|
dispatch('closeToast');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex-shrink-0 self-top -translate-y-0.5">
|
<div class="shrink-0 self-top -translate-y-0.5">
|
||||||
<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
|
<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,10 @@
|
|||||||
<SlideShow duration={5000} />
|
<SlideShow duration={5000} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-full h-full absolute top-0 left-0 bg-gradient-to-t from-20% from-black to-transparent"
|
class="w-full h-full absolute top-0 left-0 bg-linear-to-t from-20% from-black to-transparent"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 backdrop-blur-sm bg-black/50"></div>
|
<div class="w-full h-full absolute top-0 left-0 backdrop-blur-xs bg-black/50"></div>
|
||||||
|
|
||||||
<div class="relative bg-transparent w-full min-h-screen flex z-10">
|
<div class="relative bg-transparent w-full min-h-screen flex z-10">
|
||||||
<div class="flex flex-col justify-end w-full items-center pb-10 text-center">
|
<div class="flex flex-col justify-end w-full items-center pb-10 text-center">
|
||||||
|
|||||||
@@ -131,14 +131,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
|
<div
|
||||||
|
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
||||||
|
>
|
||||||
{#if (feedbacks ?? []).length === 0}
|
{#if (feedbacks ?? []).length === 0}
|
||||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
||||||
{$i18n.t('No feedbacks found')}
|
{$i18n.t('No feedbacks found')}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<table
|
<table
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
|
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
|
||||||
>
|
>
|
||||||
<thead
|
<thead
|
||||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
||||||
@@ -169,7 +171,7 @@
|
|||||||
<td class=" py-0.5 text-right font-semibold">
|
<td class=" py-0.5 text-right font-semibold">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<Tooltip content={feedback?.user?.name}>
|
<Tooltip content={feedback?.user?.name}>
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<img
|
<img
|
||||||
src={feedback?.user?.profile_image_url ?? '/user.png'}
|
src={feedback?.user?.profile_image_url ?? '/user.png'}
|
||||||
alt={feedback?.user?.name}
|
alt={feedback?.user?.name}
|
||||||
|
|||||||
@@ -288,7 +288,7 @@
|
|||||||
<MagnifyingGlass className="size-3" />
|
<MagnifyingGlass className="size-3" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
placeholder={$i18n.t('Search')}
|
placeholder={$i18n.t('Search')}
|
||||||
on:focus={() => {
|
on:focus={() => {
|
||||||
@@ -300,7 +300,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
|
<div
|
||||||
|
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
||||||
|
>
|
||||||
{#if loadingLeaderboard}
|
{#if loadingLeaderboard}
|
||||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
@@ -349,7 +351,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 flex flex-col justify-center">
|
<td class="px-3 py-1.5 flex flex-col justify-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<img
|
<img
|
||||||
src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
|
src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
|
||||||
alt={model.name}
|
alt={model.name}
|
||||||
|
|||||||
@@ -180,12 +180,12 @@
|
|||||||
|
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', onKeyUp);
|
||||||
window.addEventListener('blur', onBlur);
|
window.addEventListener('blur-sm', onBlur);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
window.removeEventListener('keyup', onKeyUp);
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('blur-sm', onBlur);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
<Search className="size-3.5" />
|
<Search className="size-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
placeholder={$i18n.t('Search Functions')}
|
placeholder={$i18n.t('Search Functions')}
|
||||||
/>
|
/>
|
||||||
@@ -241,14 +241,14 @@
|
|||||||
<div class=" flex-1 self-center pl-1">
|
<div class=" flex-1 self-center pl-1">
|
||||||
<div class=" font-semibold flex items-center gap-1.5">
|
<div class=" font-semibold flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{func.type}
|
{func.type}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if func?.meta?.manifest?.version}
|
{#if func?.meta?.manifest?.version}
|
||||||
<div
|
<div
|
||||||
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class="text-xs font-bold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
v{func?.meta?.manifest?.version ?? ''}
|
v{func?.meta?.manifest?.version ?? ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-1.5 px-1">
|
<div class="flex gap-1.5 px-1">
|
||||||
<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{func.id}</div>
|
<div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div>
|
||||||
|
|
||||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||||
{func.meta.description}
|
{func.meta.description}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -300,7 +301,7 @@ class Pipe:
|
|||||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
||||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
<div class="w-full mb-2 flex flex-col gap-0.5">
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class=" flex-shrink-0 mr-2">
|
<div class=" shrink-0 mr-2">
|
||||||
<Tooltip content={$i18n.t('Back')}>
|
<Tooltip content={$i18n.t('Back')}>
|
||||||
<button
|
<button
|
||||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||||
@@ -317,7 +318,7 @@ class Pipe:
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
|
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
|
||||||
<input
|
<input
|
||||||
class="w-full text-2xl font-medium bg-transparent outline-none font-primary"
|
class="w-full text-2xl font-medium bg-transparent outline-hidden font-primary"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Function Name')}
|
placeholder={$i18n.t('Function Name')}
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
@@ -333,13 +334,13 @@ class Pipe:
|
|||||||
|
|
||||||
<div class=" flex gap-2 px-1 items-center">
|
<div class=" flex gap-2 px-1 items-center">
|
||||||
{#if edit}
|
{#if edit}
|
||||||
<div class="text-sm text-gray-500 flex-shrink-0">
|
<div class="text-sm text-gray-500 shrink-0">
|
||||||
{id}
|
{id}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
|
<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Function ID')}
|
placeholder={$i18n.t('Function ID')}
|
||||||
bind:value={id}
|
bind:value={id}
|
||||||
@@ -355,7 +356,7 @@ class Pipe:
|
|||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-none"
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Function Description')}
|
placeholder={$i18n.t('Function Description')}
|
||||||
bind:value={meta.description}
|
bind:value={meta.description}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
||||||
|
|
||||||
<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"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import ChartBar from '../icons/ChartBar.svelte';
|
import ChartBar from '../icons/ChartBar.svelte';
|
||||||
import DocumentChartBar from '../icons/DocumentChartBar.svelte';
|
import DocumentChartBar from '../icons/DocumentChartBar.svelte';
|
||||||
import Evaluations from './Settings/Evaluations.svelte';
|
import Evaluations from './Settings/Evaluations.svelte';
|
||||||
import CodeInterpreter from './Settings/CodeInterpreter.svelte';
|
import CodeExecution from './Settings/CodeExecution.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -191,11 +191,11 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
'code-interpreter'
|
'code-execution'
|
||||||
? ''
|
? ''
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'code-interpreter';
|
selectedTab = 'code-execution';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Code Interpreter')}</div>
|
<div class=" self-center">{$i18n.t('Code Execution')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -391,8 +391,8 @@
|
|||||||
await config.set(await getBackendConfig());
|
await config.set(await getBackendConfig());
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'code-interpreter'}
|
{:else if selectedTab === 'code-execution'}
|
||||||
<CodeInterpreter
|
<CodeExecution
|
||||||
saveHandler={async () => {
|
saveHandler={async () => {
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||||
bind:value={STT_ENGINE}
|
bind:value={STT_ENGINE}
|
||||||
placeholder="Select an engine"
|
placeholder="Select an engine"
|
||||||
>
|
>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full bg-transparent outline-none"
|
class="flex-1 w-full bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={STT_OPENAI_API_BASE_URL}
|
bind:value={STT_OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-2" />
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="model-list"
|
list="model-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={STT_MODEL}
|
bind:value={STT_MODEL}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
/>
|
/>
|
||||||
@@ -224,14 +224,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-2" />
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</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-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={STT_MODEL}
|
bind:value={STT_MODEL}
|
||||||
placeholder="Select a model (optional)"
|
placeholder="Select a model (optional)"
|
||||||
/>
|
/>
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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-none"
|
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('Set whisper model')}
|
placeholder={$i18n.t('Set whisper model')}
|
||||||
bind:value={STT_WHISPER_MODEL}
|
bind:value={STT_WHISPER_MODEL}
|
||||||
/>
|
/>
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800" />
|
<hr class="border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||||
bind:value={TTS_ENGINE}
|
bind:value={TTS_ENGINE}
|
||||||
placeholder="Select a mode"
|
placeholder="Select a mode"
|
||||||
on:change={async (e) => {
|
on:change={async (e) => {
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full bg-transparent outline-none"
|
class="flex-1 w-full bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={TTS_OPENAI_API_BASE_URL}
|
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
bind:value={TTS_API_KEY}
|
bind:value={TTS_API_KEY}
|
||||||
required
|
required
|
||||||
@@ -396,13 +396,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-1 flex gap-2 mb-1">
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
bind:value={TTS_API_KEY}
|
bind:value={TTS_API_KEY}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
placeholder={$i18n.t('Azure Region')}
|
placeholder={$i18n.t('Azure Region')}
|
||||||
bind:value={TTS_AZURE_SPEECH_REGION}
|
bind:value={TTS_AZURE_SPEECH_REGION}
|
||||||
required
|
required
|
||||||
@@ -411,7 +411,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-2" />
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
{#if TTS_ENGINE === ''}
|
{#if TTS_ENGINE === ''}
|
||||||
<div>
|
<div>
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_VOICE}
|
bind:value={TTS_VOICE}
|
||||||
>
|
>
|
||||||
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
||||||
@@ -442,7 +442,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="model-list"
|
list="model-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_MODEL}
|
bind:value={TTS_MODEL}
|
||||||
placeholder="CMU ARCTIC speaker embedding name"
|
placeholder="CMU ARCTIC speaker embedding name"
|
||||||
/>
|
/>
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="voice-list"
|
list="voice-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_VOICE}
|
bind:value={TTS_VOICE}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
/>
|
/>
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="tts-model-list"
|
list="tts-model-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_MODEL}
|
bind:value={TTS_MODEL}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
/>
|
/>
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="voice-list"
|
list="voice-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_VOICE}
|
bind:value={TTS_VOICE}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
/>
|
/>
|
||||||
@@ -544,7 +544,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="tts-model-list"
|
list="tts-model-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_MODEL}
|
bind:value={TTS_MODEL}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
/>
|
/>
|
||||||
@@ -566,7 +566,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="voice-list"
|
list="voice-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_VOICE}
|
bind:value={TTS_VOICE}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
/>
|
/>
|
||||||
@@ -593,7 +593,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
list="tts-model-list"
|
list="tts-model-list"
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
|
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
|
||||||
placeholder="Select a output format"
|
placeholder="Select a output format"
|
||||||
/>
|
/>
|
||||||
@@ -603,13 +603,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="dark:border-gray-850 my-2" />
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div class="pt-0.5 flex w-full justify-between">
|
<div class="pt-0.5 flex w-full justify-between">
|
||||||
<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
|
<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||||
aria-label="Select how to split message text for TTS requests"
|
aria-label="Select how to split message text for TTS requests"
|
||||||
bind:value={TTS_SPLIT_ON}
|
bind:value={TTS_SPLIT_ON}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { getCodeExecutionConfig, setCodeExecutionConfig } from '$lib/apis/configs';
|
||||||
|
|
||||||
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
let config = null;
|
||||||
|
|
||||||
|
let engines = ['pyodide', 'jupyter'];
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
const res = await setCodeExecutionConfig(localStorage.token, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const res = await getCodeExecutionConfig(localStorage.token);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
config = res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
on:submit|preventDefault={async () => {
|
||||||
|
await submitHandler();
|
||||||
|
saveHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
|
{#if config}
|
||||||
|
<div>
|
||||||
|
<div class="mb-3.5">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class="mb-2.5">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||||
|
bind:value={config.CODE_EXECUTION_ENGINE}
|
||||||
|
placeholder={$i18n.t('Select a engine')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||||
|
{#each engines as engine}
|
||||||
|
<option value={engine}>{engine}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
|
||||||
|
<div class="text-gray-500 text-xs">
|
||||||
|
{$i18n.t(
|
||||||
|
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
|
||||||
|
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||||
|
<div class="text-xs font-medium">
|
||||||
|
{$i18n.t('Jupyter URL')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Enter Jupyter URL')}
|
||||||
|
bind:value={config.CODE_EXECUTION_JUPYTER_URL}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||||
|
<div class=" flex gap-2 w-full items-center justify-between">
|
||||||
|
<div class="text-xs font-medium">
|
||||||
|
{$i18n.t('Jupyter Auth')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
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}
|
||||||
|
placeholder={$i18n.t('Select an auth method')}
|
||||||
|
>
|
||||||
|
<option selected value="">{$i18n.t('None')}</option>
|
||||||
|
<option value="token">{$i18n.t('Token')}</option>
|
||||||
|
<option value="password">{$i18n.t('Password')}</option>
|
||||||
|
</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>
|
||||||
|
{/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}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3.5">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class="mb-2.5">
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Enable Code Interpreter')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if config.ENABLE_CODE_INTERPRETER}
|
||||||
|
<div class="mb-2.5">
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Code Interpreter Engine')}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||||
|
bind:value={config.CODE_INTERPRETER_ENGINE}
|
||||||
|
placeholder={$i18n.t('Select a engine')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||||
|
{#each engines as engine}
|
||||||
|
<option value={engine}>{engine}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
|
||||||
|
<div class="text-gray-500 text-xs">
|
||||||
|
{$i18n.t(
|
||||||
|
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
|
||||||
|
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||||
|
<div class="text-xs font-medium">
|
||||||
|
{$i18n.t('Jupyter URL')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Enter Jupyter URL')}
|
||||||
|
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||||
|
<div class="flex gap-2 w-full items-center justify-between">
|
||||||
|
<div class="text-xs font-medium">
|
||||||
|
{$i18n.t('Jupyter Auth')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
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}
|
||||||
|
placeholder={$i18n.t('Select an auth method')}
|
||||||
|
>
|
||||||
|
<option selected value="">{$i18n.t('None')}</option>
|
||||||
|
<option value="token">{$i18n.t('Token')}</option>
|
||||||
|
<option value="password">{$i18n.t('Password')}</option>
|
||||||
|
</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>
|
||||||
|
{/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}
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="py-0.5 w-full">
|
||||||
|
<div class=" mb-2.5 text-xs font-medium">
|
||||||
|
{$i18n.t('Code Interpreter Prompt Template')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
|
||||||
|
placeholder={$i18n.t(
|
||||||
|
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import { onMount, getContext } from 'svelte';
|
|
||||||
import { getCodeInterpreterConfig, setCodeInterpreterConfig } from '$lib/apis/configs';
|
|
||||||
|
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
|
||||||
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let saveHandler: Function;
|
|
||||||
|
|
||||||
let config = null;
|
|
||||||
|
|
||||||
let engines = ['pyodide', 'jupyter'];
|
|
||||||
|
|
||||||
const submitHandler = async () => {
|
|
||||||
const res = await setCodeInterpreterConfig(localStorage.token, config);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const res = await getCodeInterpreterConfig(localStorage.token);
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
config = res;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
|
||||||
on:submit|preventDefault={async () => {
|
|
||||||
await submitHandler();
|
|
||||||
saveHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
|
||||||
{#if config}
|
|
||||||
<div>
|
|
||||||
<div class=" mb-1 text-sm font-medium">
|
|
||||||
{$i18n.t('Code Interpreter')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
|
||||||
{$i18n.t('Enable Code Interpreter')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Code Interpreter Engine')}</div>
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<select
|
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
|
||||||
bind:value={config.CODE_INTERPRETER_ENGINE}
|
|
||||||
placeholder={$i18n.t('Select a engine')}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
|
||||||
{#each engines as engine}
|
|
||||||
<option value={engine}>{engine}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
|
|
||||||
<div class="mt-1 flex flex-col gap-1.5 mb-1 w-full">
|
|
||||||
<div class="text-xs font-medium">
|
|
||||||
{$i18n.t('Jupyter URL')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex w-full">
|
|
||||||
<div class="flex-1">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-none"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Enter Jupyter URL')}
|
|
||||||
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-1 flex gap-2 mb-1 w-full items-center justify-between">
|
|
||||||
<div class="text-xs font-medium">
|
|
||||||
{$i18n.t('Jupyter Auth')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<select
|
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-left"
|
|
||||||
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
|
|
||||||
placeholder={$i18n.t('Select an auth method')}
|
|
||||||
>
|
|
||||||
<option selected value="">{$i18n.t('None')}</option>
|
|
||||||
<option value="token">{$i18n.t('Token')}</option>
|
|
||||||
<option value="password">{$i18n.t('Password')}</option>
|
|
||||||
</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>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="py-0.5 w-full">
|
|
||||||
<div class=" mb-2.5 text-xs font-medium">
|
|
||||||
{$i18n.t('Code Interpreter Prompt Template')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<Textarea
|
|
||||||
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
|
|
||||||
placeholder={$i18n.t(
|
|
||||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
|
||||||
<button
|
|
||||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{$i18n.t('Save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ENABLE_OPENAI_API}
|
{#if ENABLE_OPENAI_API}
|
||||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
@@ -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}
|
||||||
@@ -283,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="pr-1.5 my-2">
|
<div class="pr-1.5 my-2">
|
||||||
<div class="flex justify-between items-center text-sm mb-2">
|
<div class="flex justify-between items-center text-sm mb-2">
|
||||||
@@ -300,7 +301,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ENABLE_OLLAMA_API}
|
{#if ENABLE_OLLAMA_API}
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
@@ -357,7 +358,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="pr-1.5 my-2">
|
<div class="pr-1.5 my-2">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex w-full justify-between items-center text-lg font-medium self-center font-primary"
|
class="flex w-full justify-between items-center text-lg font-medium self-center font-primary"
|
||||||
>
|
>
|
||||||
<div class=" flex-shrink-0">
|
<div class=" shrink-0">
|
||||||
{$i18n.t('Manage Ollama')}
|
{$i18n.t('Manage Ollama')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-none"
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 relative">
|
<div class="flex-1 relative">
|
||||||
<input
|
<input
|
||||||
class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
|
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
inputClassName=" outline-none bg-transparent w-full"
|
inputClassName=" outline-hidden bg-transparent w-full"
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
bind:value={key}
|
bind:value={key}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850 my-1" />
|
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
||||||
|
|
||||||
{#if $config?.features.enable_admin_export ?? true}
|
{#if $config?.features.enable_admin_export ?? true}
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
@@ -245,7 +245,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
placeholder={$i18n.t('Model Name')}
|
placeholder={$i18n.t('Model Name')}
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={id}
|
bind:value={id}
|
||||||
placeholder={$i18n.t('Model ID')}
|
placeholder={$i18n.t('Model ID')}
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
placeholder={$i18n.t('Enter description')}
|
placeholder={$i18n.t('Enter description')}
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||||
{$models.find((model) => model.id === modelId)?.name}
|
{$models.find((model) => model.id === modelId)?.name}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
<select
|
<select
|
||||||
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||||
bind:value={selectedModelId}
|
bind:value={selectedModelId}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Select a model')}</option>
|
<option value="">{$i18n.t('Select a model')}</option>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="flex-shrink-0 line-clamp-1">
|
<div class="shrink-0 line-clamp-1">
|
||||||
{model.name}
|
{model.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||||
import {
|
import {
|
||||||
getAdminConfig,
|
getAdminConfig,
|
||||||
getLdapConfig,
|
getLdapConfig,
|
||||||
@@ -11,7 +11,9 @@
|
|||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { config } from '$lib/stores';
|
import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants';
|
||||||
|
import { config, showChangelog } from '$lib/stores';
|
||||||
|
import { compareVersion } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
@@ -19,6 +21,12 @@
|
|||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
let updateAvailable = null;
|
||||||
|
let version = {
|
||||||
|
current: '',
|
||||||
|
latest: ''
|
||||||
|
};
|
||||||
|
|
||||||
let adminConfig = null;
|
let adminConfig = null;
|
||||||
let webhookUrl = '';
|
let webhookUrl = '';
|
||||||
|
|
||||||
@@ -39,6 +47,21 @@
|
|||||||
ciphers: ''
|
ciphers: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkForVersionUpdates = async () => {
|
||||||
|
updateAvailable = null;
|
||||||
|
version = await getVersionUpdates(localStorage.token).catch((error) => {
|
||||||
|
return {
|
||||||
|
current: WEBUI_VERSION,
|
||||||
|
latest: WEBUI_VERSION
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(version);
|
||||||
|
|
||||||
|
updateAvailable = compareVersion(version.latest, version.current);
|
||||||
|
console.log(updateAvailable);
|
||||||
|
};
|
||||||
|
|
||||||
const updateLdapServerHandler = async () => {
|
const updateLdapServerHandler = async () => {
|
||||||
if (!ENABLE_LDAP) return;
|
if (!ENABLE_LDAP) return;
|
||||||
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
|
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
|
||||||
@@ -63,6 +86,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
checkForVersionUpdates();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
adminConfig = await getAdminConfig(localStorage.token);
|
adminConfig = await getAdminConfig(localStorage.token);
|
||||||
@@ -87,381 +112,511 @@
|
|||||||
updateHandler();
|
updateHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
<div class="mt-0.5 space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if adminConfig !== null}
|
{#if adminConfig !== null}
|
||||||
<div>
|
<div class="">
|
||||||
<div class=" mb-3 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
<div class="mb-3.5">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between pr-2">
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
|
<div class="mb-2.5">
|
||||||
</div>
|
<div class=" mb-1 text-xs font-medium flex space-x-2 items-center">
|
||||||
|
<div>
|
||||||
<div class=" my-3 flex w-full justify-between">
|
{$i18n.t('Version')}
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
</div>
|
||||||
<div class="flex items-center relative">
|
|
||||||
<select
|
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
|
||||||
bind:value={adminConfig.DEFAULT_USER_ROLE}
|
|
||||||
placeholder="Select a role"
|
|
||||||
>
|
|
||||||
<option value="pending">{$i18n.t('pending')}</option>
|
|
||||||
<option value="user">{$i18n.t('user')}</option>
|
|
||||||
<option value="admin">{$i18n.t('admin')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex w-full justify-between pr-2 my-3">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
|
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.ENABLE_API_KEY} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if adminConfig?.ENABLE_API_KEY}
|
|
||||||
<div class=" flex w-full justify-between pr-2 my-3">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
|
||||||
{$i18n.t('API Key Endpoint Restrictions')}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex w-full justify-between items-center">
|
||||||
|
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Tooltip content={WEBUI_BUILD_HASH}>
|
||||||
|
v{WEBUI_VERSION}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
|
<a
|
||||||
</div>
|
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{updateAvailable === null
|
||||||
|
? $i18n.t('Checking for updates...')
|
||||||
|
: updateAvailable
|
||||||
|
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||||
|
: $i18n.t('(latest)')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if adminConfig?.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS}
|
<button
|
||||||
<div class=" flex w-full flex-col pr-2">
|
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
|
||||||
<div class=" text-xs font-medium">
|
type="button"
|
||||||
{$i18n.t('Allowed Endpoints')}
|
on:click={() => {
|
||||||
|
showChangelog.set(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{$i18n.t("See what's new")}</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<button
|
||||||
class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-none"
|
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||||
type="text"
|
type="button"
|
||||||
placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
|
on:click={() => {
|
||||||
bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
|
checkForVersionUpdates();
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Check for updates')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mb-2.5">
|
||||||
<!-- https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints -->
|
<div class="flex w-full justify-between items-center">
|
||||||
<a
|
<div class="text-xs pr-2">
|
||||||
href="https://docs.openwebui.com/getting-started/api-endpoints"
|
<div class="">
|
||||||
target="_blank"
|
{$i18n.t('Help')}
|
||||||
class=" text-gray-300 font-medium underline"
|
</div>
|
||||||
>
|
<div class=" text-xs text-gray-500">
|
||||||
{$i18n.t('To learn more about available endpoints, visit our documentation.')}
|
{$i18n.t('Discover how to use Open WebUI and seek support from the community.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="flex-shrink-0 text-xs font-medium underline"
|
||||||
|
href="https://docs.openwebui.com/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{$i18n.t('Documentation')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||||
|
<img
|
||||||
|
alt="Discord"
|
||||||
|
src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||||
|
<img
|
||||||
|
alt="X (formerly Twitter) Follow"
|
||||||
|
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||||
|
<img
|
||||||
|
alt="Github Repo"
|
||||||
|
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
|
||||||
{$i18n.t('Show Admin Details in Account Pending Overlay')}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
|
<div class="mb-2.5">
|
||||||
</div>
|
<div class="flex w-full justify-between items-center">
|
||||||
|
<div class="text-xs pr-2">
|
||||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
<div class="">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
{$i18n.t('License')}
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
|
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
type="text"
|
|
||||||
placeholder={`e.g.) "http://localhost:3000"`}
|
|
||||||
bind:value={adminConfig.WEBUI_URL}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
{$i18n.t(
|
|
||||||
'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
type="text"
|
|
||||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
|
||||||
bind:value={adminConfig.JWT_EXPIRES_IN}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
{$i18n.t('Valid time units:')}
|
|
||||||
<span class=" text-gray-300 font-medium"
|
|
||||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
||||||
type="text"
|
|
||||||
placeholder={`https://example.com/webhook`}
|
|
||||||
bind:value={webhookUrl}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div class="pt-1 flex w-full justify-between pr-2">
|
|
||||||
<div class=" self-center text-sm font-medium">
|
|
||||||
{$i18n.t('Channels')} ({$i18n.t('Beta')})
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
|
||||||
|
|
||||||
<div class=" space-y-3">
|
|
||||||
<div class="mt-2 space-y-2 pr-1.5">
|
|
||||||
<div class="flex justify-between items-center text-sm">
|
|
||||||
<div class=" font-medium">{$i18n.t('LDAP')}</div>
|
|
||||||
|
|
||||||
<div class="mt-1">
|
|
||||||
<Switch
|
|
||||||
bind:state={ENABLE_LDAP}
|
|
||||||
on:change={async () => {
|
|
||||||
updateLdapConfig(localStorage.token, ENABLE_LDAP);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if ENABLE_LDAP}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Label')}
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<a
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
class=" text-xs text-gray-500 hover:underline"
|
||||||
required
|
href="https://docs.openwebui.com/enterprise"
|
||||||
placeholder={$i18n.t('Enter server label')}
|
target="_blank"
|
||||||
bind:value={LDAP_SERVER.label}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="w-full"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Host')}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
|
||||||
required
|
|
||||||
placeholder={$i18n.t('Enter server host')}
|
|
||||||
bind:value={LDAP_SERVER.host}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Port')}
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
placement="top-start"
|
|
||||||
content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
|
|
||||||
className="w-full"
|
|
||||||
>
|
>
|
||||||
<input
|
{$i18n.t(
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
|
||||||
type="number"
|
|
||||||
placeholder={$i18n.t('Enter server port')}
|
|
||||||
bind:value={LDAP_SERVER.port}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Application DN')}
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t('The Application Account DN you bind with for search')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
|
||||||
required
|
|
||||||
placeholder={$i18n.t('Enter Application DN')}
|
|
||||||
bind:value={LDAP_SERVER.app_dn}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Application DN Password')}
|
|
||||||
</div>
|
|
||||||
<SensitiveInput
|
|
||||||
placeholder={$i18n.t('Enter Application DN Password')}
|
|
||||||
bind:value={LDAP_SERVER.app_dn_password}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Attribute for Mail')}
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t(
|
|
||||||
'The LDAP attribute that maps to the mail that users use to sign in.'
|
|
||||||
)}
|
)}
|
||||||
placement="top-start"
|
</a>
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
|
||||||
required
|
|
||||||
placeholder={$i18n.t('Example: mail')}
|
|
||||||
bind:value={LDAP_SERVER.attribute_for_mail}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
<!-- <button
|
||||||
<div class="w-full">
|
class="flex-shrink-0 text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Attribute for Username')}
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t(
|
|
||||||
'The LDAP attribute that maps to the username that users use to sign in.'
|
|
||||||
)}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
|
||||||
required
|
|
||||||
placeholder={$i18n.t('Example: sAMAccountName or uid or userPrincipalName')}
|
|
||||||
bind:value={LDAP_SERVER.attribute_for_username}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Search Base')}
|
|
||||||
</div>
|
|
||||||
<Tooltip content={$i18n.t('The base to search for users')} placement="top-start">
|
|
||||||
<input
|
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
|
||||||
required
|
|
||||||
placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
|
|
||||||
bind:value={LDAP_SERVER.search_base}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Search Filters')}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
|
||||||
placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
|
|
||||||
bind:value={LDAP_SERVER.search_filters}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
<a
|
|
||||||
class=" text-gray-300 font-medium underline"
|
|
||||||
href="https://ldap.com/ldap-filters/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
{$i18n.t('Click here for filter guides.')}
|
{$i18n.t('Activate')}
|
||||||
</a>
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</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">{$i18n.t('Default User Role')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||||
|
bind:value={adminConfig.DEFAULT_USER_ROLE}
|
||||||
|
placeholder="Select a role"
|
||||||
|
>
|
||||||
|
<option value="pending">{$i18n.t('pending')}</option>
|
||||||
|
<option value="user">{$i18n.t('user')}</option>
|
||||||
|
<option value="admin">{$i18n.t('admin')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" mb-2.5 flex w-full justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Show Admin Details in Account Pending Overlay')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_API_KEY} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if adminConfig?.ENABLE_API_KEY}
|
||||||
|
<div class="mb-2.5 flex w-full justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('API Key Endpoint Restrictions')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if adminConfig?.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS}
|
||||||
|
<div class=" flex w-full flex-col pr-2">
|
||||||
|
<div class=" text-xs font-medium">
|
||||||
|
{$i18n.t('Allowed Endpoints')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
|
||||||
|
bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
<!-- https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints -->
|
||||||
|
<a
|
||||||
|
href="https://docs.openwebui.com/getting-started/api-endpoints"
|
||||||
|
target="_blank"
|
||||||
|
class=" text-gray-300 font-medium underline"
|
||||||
|
>
|
||||||
|
{$i18n.t('To learn more about available endpoints, visit our documentation.')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class=" mb-2.5 w-full justify-between">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2 space-x-2">
|
||||||
|
<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"
|
||||||
|
type="text"
|
||||||
|
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||||
|
bind:value={adminConfig.JWT_EXPIRES_IN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t('Valid time units:')}
|
||||||
|
<span class=" text-gray-300 font-medium"
|
||||||
|
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" space-y-3">
|
||||||
|
<div class="mt-2 space-y-2 pr-1.5">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('TLS')}</div>
|
<div class=" font-medium">{$i18n.t('LDAP')}</div>
|
||||||
|
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<Switch bind:state={LDAP_SERVER.use_tls} />
|
<Switch
|
||||||
|
bind:state={ENABLE_LDAP}
|
||||||
|
on:change={async () => {
|
||||||
|
updateLdapConfig(localStorage.token, ENABLE_LDAP);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if LDAP_SERVER.use_tls}
|
|
||||||
<div class="flex w-full gap-2">
|
{#if ENABLE_LDAP}
|
||||||
<div class="w-full">
|
<div class="flex flex-col gap-1">
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
|
<div class="flex w-full gap-2">
|
||||||
{$i18n.t('Certificate Path')}
|
<div class="w-full">
|
||||||
</div>
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
<input
|
{$i18n.t('Label')}
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
</div>
|
||||||
required
|
|
||||||
placeholder={$i18n.t('Enter certificate path')}
|
|
||||||
bind:value={LDAP_SERVER.certificate_path}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
|
||||||
{$i18n.t('Ciphers')}
|
|
||||||
</div>
|
|
||||||
<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
|
|
||||||
<input
|
<input
|
||||||
class="w-full bg-transparent outline-none py-0.5"
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
placeholder={$i18n.t('Example: ALL')}
|
required
|
||||||
bind:value={LDAP_SERVER.ciphers}
|
placeholder={$i18n.t('Enter server label')}
|
||||||
|
bind:value={LDAP_SERVER.label}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
|
<div class="w-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Host')}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t('Enter server host')}
|
||||||
|
bind:value={LDAP_SERVER.host}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Port')}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
placement="top-start"
|
||||||
|
content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
type="number"
|
||||||
|
placeholder={$i18n.t('Enter server port')}
|
||||||
|
bind:value={LDAP_SERVER.port}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Application DN')}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('The Application Account DN you bind with for search')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t('Enter Application DN')}
|
||||||
|
bind:value={LDAP_SERVER.app_dn}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Application DN Password')}
|
||||||
|
</div>
|
||||||
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('Enter Application DN Password')}
|
||||||
|
bind:value={LDAP_SERVER.app_dn_password}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Attribute for Mail')}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'The LDAP attribute that maps to the mail that users use to sign in.'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t('Example: mail')}
|
||||||
|
bind:value={LDAP_SERVER.attribute_for_mail}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Attribute for Username')}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'The LDAP attribute that maps to the username that users use to sign in.'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t(
|
||||||
|
'Example: sAMAccountName or uid or userPrincipalName'
|
||||||
|
)}
|
||||||
|
bind:value={LDAP_SERVER.attribute_for_username}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Search Base')}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('The base to search for users')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
|
||||||
|
bind:value={LDAP_SERVER.search_base}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Search Filters')}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
|
||||||
|
bind:value={LDAP_SERVER.search_filters}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
<a
|
||||||
|
class=" text-gray-300 font-medium underline"
|
||||||
|
href="https://ldap.com/ldap-filters/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{$i18n.t('Click here for filter guides.')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<div class=" font-medium">{$i18n.t('TLS')}</div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<Switch bind:state={LDAP_SERVER.use_tls} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if LDAP_SERVER.use_tls}
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
|
||||||
|
{$i18n.t('Certificate Path')}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
required
|
||||||
|
placeholder={$i18n.t('Enter certificate path')}
|
||||||
|
bind:value={LDAP_SERVER.certificate_path}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||||
|
{$i18n.t('Ciphers')}
|
||||||
|
</div>
|
||||||
|
<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden py-0.5"
|
||||||
|
placeholder={$i18n.t('Example: ALL')}
|
||||||
|
bind:value={LDAP_SERVER.ciphers}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="w-full"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full"></div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Enable Community Sharing')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Channels')} ({$i18n.t('Beta')})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 w-full justify-between">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2 space-x-2">
|
||||||
|
<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"
|
||||||
|
type="text"
|
||||||
|
placeholder={`e.g.) "http://localhost:3000"`}
|
||||||
|
bind:value={adminConfig.WEBUI_URL}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" w-full justify-between">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2 space-x-2">
|
||||||
|
<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"
|
||||||
|
type="text"
|
||||||
|
placeholder={`https://example.com/webhook`}
|
||||||
|
bind:value={webhookUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user