Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fedd72e39 | ||
|
|
472a291c24 | ||
|
|
b124c15018 | ||
|
|
0a7a265ff5 | ||
|
|
93d486d50e | ||
|
|
af10aba805 | ||
|
|
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 | ||
|
|
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 | ||
|
|
c47eddce32 | ||
|
|
e98f859a14 | ||
|
|
86074cff6c | ||
|
|
3ac3615ecd | ||
|
|
d590647d09 | ||
|
|
e4d7d41df6 | ||
|
|
a461861030 | ||
|
|
40d4db97e6 | ||
|
|
2c711d8365 | ||
|
|
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
|
||||||
|
|||||||
+65
-4
@@ -5,6 +5,71 @@ 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.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 +94,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.
|
||||||
|
|||||||
+163
-10
@@ -2,6 +2,8 @@ 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
|
||||||
@@ -593,8 +595,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 +603,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 +656,16 @@ if CUSTOM_NAME:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# LICENSE_KEY
|
||||||
|
####################################
|
||||||
|
|
||||||
|
LICENSE_KEY = PersistentConfig(
|
||||||
|
"LICENSE_KEY",
|
||||||
|
"license.key",
|
||||||
|
os.environ.get("LICENSE_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# STORAGE PROVIDER
|
# STORAGE PROVIDER
|
||||||
####################################
|
####################################
|
||||||
@@ -668,6 +684,10 @@ 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 +787,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"
|
||||||
@@ -1190,6 +1213,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 +1370,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 +1429,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"),
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1505,6 +1594,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 +1714,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 +1795,12 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
|
|||||||
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig(
|
||||||
|
"RAG_WEB_SEARCH_FULL_CONTEXT",
|
||||||
|
"rag.web.search.full_context",
|
||||||
|
os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "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 +1904,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 +1948,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 +2188,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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+74
-18
@@ -88,6 +88,7 @@ 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,
|
||||||
@@ -99,7 +100,13 @@ from open_webui.config import (
|
|||||||
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 +114,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 +133,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 +149,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 +160,7 @@ from open_webui.config import (
|
|||||||
# Retrieval
|
# Retrieval
|
||||||
RAG_TEMPLATE,
|
RAG_TEMPLATE,
|
||||||
DEFAULT_RAG_TEMPLATE,
|
DEFAULT_RAG_TEMPLATE,
|
||||||
|
RAG_FULL_CONTEXT,
|
||||||
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,
|
||||||
@@ -173,12 +188,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,
|
||||||
|
RAG_WEB_SEARCH_FULL_CONTEXT,
|
||||||
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,
|
||||||
@@ -264,6 +283,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,
|
||||||
@@ -310,15 +330,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 +367,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.
|
||||||
@@ -365,6 +387,9 @@ async def lifespan(app: FastAPI):
|
|||||||
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 +401,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 +508,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 +524,8 @@ 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.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
|
||||||
@@ -529,6 +560,7 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
|
|||||||
|
|
||||||
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
|
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
|
||||||
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
|
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
|
||||||
|
app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
|
||||||
app.state.config.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
|
||||||
@@ -546,6 +578,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 +587,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 +635,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 +660,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 +675,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 +741,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
|
||||||
@@ -934,7 +987,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 +1116,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": {
|
||||||
@@ -1102,6 +1155,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,
|
||||||
@@ -1198,7 +1254,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 +1265,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 +1299,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}"}"/>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ 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.env import (
|
from open_webui.env import (
|
||||||
@@ -84,6 +85,19 @@ def query_doc(
|
|||||||
raise 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:
|
||||||
|
print(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def query_doc_with_hybrid_search(
|
def query_doc_with_hybrid_search(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
query: str,
|
query: str,
|
||||||
@@ -137,6 +151,27 @@ def query_doc_with_hybrid_search(
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def merge_get_results(get_results: list[dict]) -> dict:
|
||||||
|
# Initialize lists to store combined data
|
||||||
|
combined_documents = []
|
||||||
|
combined_metadatas = []
|
||||||
|
combined_ids = []
|
||||||
|
|
||||||
|
for data in get_results:
|
||||||
|
combined_documents.extend(data["documents"][0])
|
||||||
|
combined_metadatas.extend(data["metadatas"][0])
|
||||||
|
combined_ids.extend(data["ids"][0])
|
||||||
|
|
||||||
|
# Create the output dictionary
|
||||||
|
result = {
|
||||||
|
"documents": [combined_documents],
|
||||||
|
"metadatas": [combined_metadatas],
|
||||||
|
"ids": [combined_ids],
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def merge_and_sort_query_results(
|
def merge_and_sort_query_results(
|
||||||
query_results: list[dict], k: int, reverse: bool = False
|
query_results: list[dict], k: int, reverse: bool = False
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
@@ -180,6 +215,23 @@ def merge_and_sort_query_results(
|
|||||||
return result
|
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(
|
||||||
collection_names: list[str],
|
collection_names: list[str],
|
||||||
queries: list[str],
|
queries: list[str],
|
||||||
@@ -297,14 +349,22 @@ 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":
|
if file.get("docs"):
|
||||||
|
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":
|
||||||
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")}]],
|
||||||
@@ -331,36 +391,43 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -266,7 +267,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# print(payload)
|
# print(payload)
|
||||||
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(
|
||||||
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 +327,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 +387,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={
|
||||||
|
|||||||
@@ -251,9 +251,19 @@ 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()
|
||||||
|
if (
|
||||||
|
request.app.state.USER_COUNT
|
||||||
|
and user_count >= request.app.state.USER_COUNT
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
role = (
|
role = (
|
||||||
"admin"
|
"admin"
|
||||||
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 +423,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 +438,12 @@ 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 request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT:
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||||
|
)
|
||||||
|
|
||||||
if not validate_email_format(form_data.email.lower()):
|
if not validate_email_format(form_data.email.lower()):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||||
@@ -437,12 +454,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 +499,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 +546,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(
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -225,17 +225,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(
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -155,6 +170,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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -224,6 +243,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 +324,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 = {
|
||||||
@@ -483,6 +512,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,
|
||||||
|
|||||||
@@ -26,7 +26,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
|
||||||
|
|
||||||
|
|
||||||
@@ -936,15 +936,28 @@ async def generate_completion(
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
print(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:
|
print(f"Connection error: {e}")
|
||||||
pass
|
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +351,7 @@ 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,
|
||||||
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
"content_extraction": {
|
"content_extraction": {
|
||||||
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
@@ -369,7 +372,8 @@ 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,
|
||||||
|
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
|
||||||
"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,
|
||||||
@@ -388,6 +392,8 @@ 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,
|
||||||
@@ -439,21 +445,26 @@ 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
|
||||||
|
RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class ConfigUpdateForm(BaseModel):
|
class ConfigUpdateForm(BaseModel):
|
||||||
|
RAG_FULL_CONTEXT: 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
|
||||||
file: Optional[FileConfig] = None
|
file: Optional[FileConfig] = None
|
||||||
@@ -473,6 +484,12 @@ 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.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
|
||||||
@@ -505,11 +522,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.RAG_WEB_SEARCH_FULL_CONTEXT = (
|
||||||
|
form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT
|
||||||
|
)
|
||||||
|
|
||||||
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 +567,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 +586,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 +596,7 @@ 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,
|
||||||
"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,
|
||||||
@@ -587,7 +616,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,
|
||||||
|
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
|
||||||
"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 +634,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 +643,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 +793,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:
|
||||||
@@ -1127,6 +1164,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 +1279,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 +1294,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 +1332,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 +1364,39 @@ 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.RAG_WEB_SEARCH_FULL_CONTEXT:
|
||||||
"status": True,
|
return {
|
||||||
"collection_name": collection_name,
|
"status": True,
|
||||||
"filenames": urls,
|
"docs": [
|
||||||
}
|
{
|
||||||
|
"content": doc.page_content,
|
||||||
|
"metadata": doc.metadata,
|
||||||
|
}
|
||||||
|
for doc in docs
|
||||||
|
],
|
||||||
|
"filenames": urls,
|
||||||
|
"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(
|
||||||
|
|||||||
@@ -58,6 +58,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 +69,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 +88,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 +125,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 +143,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 +208,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,
|
||||||
}
|
}
|
||||||
@@ -560,7 +571,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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,45 +4,76 @@ 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
|
||||||
|
|
||||||
|
|
||||||
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 +85,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()
|
||||||
|
|||||||
@@ -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*/
|
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ from open_webui.config import (
|
|||||||
S3_SECRET_ACCESS_KEY,
|
S3_SECRET_ACCESS_KEY,
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class StorageProvider(ABC):
|
class StorageProvider(ABC):
|
||||||
@@ -221,6 +227,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 +302,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)
|
||||||
|
|||||||
@@ -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,7 +14,7 @@ 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
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request, Response, status
|
from fastapi import Depends, HTTPException, Request, Response, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
@@ -24,6 +30,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:
|
||||||
|
print(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:
|
||||||
|
print(
|
||||||
|
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
print(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")
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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,94 @@ 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 request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
|
||||||
"type": "web_search_results",
|
files.append(
|
||||||
"urls": results["filenames"],
|
{
|
||||||
}
|
"docs": results.get("docs", []),
|
||||||
)
|
"name": searchQuery,
|
||||||
form_data["files"] = files
|
"type": "web_search_docs",
|
||||||
else:
|
"urls": results["filenames"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
files.append(
|
||||||
|
{
|
||||||
|
"collection_name": results["collection_name"],
|
||||||
|
"name": searchQuery,
|
||||||
|
"type": "web_search_results",
|
||||||
|
"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,
|
||||||
},
|
},
|
||||||
@@ -552,9 +565,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 +695,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,17 +736,6 @@ 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
|
||||||
@@ -778,7 +799,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
|
|
||||||
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 +816,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 +1024,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}",
|
||||||
{
|
{
|
||||||
@@ -1151,6 +1173,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
|
||||||
|
|
||||||
@@ -1301,7 +1363,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",
|
||||||
@@ -1542,7 +1619,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 +1684,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 +1761,7 @@ async def process_chat_response(
|
|||||||
== "password"
|
== "password"
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
output = {
|
output = {
|
||||||
@@ -1842,6 +1906,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}",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -225,10 +225,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"
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,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 None
|
||||||
|
|
||||||
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,21 @@ 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 (
|
||||||
|
request.app.state.USER_COUNT
|
||||||
|
and user_count >= request.app.state.USER_COUNT
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
403,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
# If the user does not exist, check if signups are enabled
|
# If the user does not exist, check if signups are enabled
|
||||||
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
||||||
# Check if an existing user with the same email already exists
|
# Check if an existing user with the same email already exists
|
||||||
existing_user = Users.get_user_by_email(
|
existing_user = Users.get_user_by_email(email)
|
||||||
user_data.get("email", "").lower()
|
|
||||||
)
|
|
||||||
if existing_user:
|
if existing_user:
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||||
|
|
||||||
@@ -334,6 +390,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 +437,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):
|
||||||
# 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,34 +212,23 @@ 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 "frequency_penalty" in openai_payload:
|
|
||||||
ollama_options["repeat_penalty"] = openai_payload["frequency_penalty"]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if "metadata" in openai_payload:
|
if "metadata" in openai_payload:
|
||||||
ollama_payload["metadata"] = openai_payload["metadata"]
|
ollama_payload["metadata"] = openai_payload["metadata"]
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -96,45 +115,7 @@ 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 if not done else None, openai_tool_calls, usage
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -48,7 +45,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 +59,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
|
||||||
@@ -106,5 +103,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
+534
-535
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.11",
|
"version": "0.5.16",
|
||||||
"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",
|
||||||
@@ -106,6 +106,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: {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-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",
|
||||||
@@ -56,6 +53,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 +66,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",
|
||||||
@@ -111,7 +109,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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -300,7 +300,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 +317,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 +333,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 +355,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}
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -283,7 +283,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 +300,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 +357,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">
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import { text } from '@sveltejs/kit';
|
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
@@ -56,6 +55,8 @@
|
|||||||
let chunkOverlap = 0;
|
let chunkOverlap = 0;
|
||||||
let pdfExtractImages = true;
|
let pdfExtractImages = true;
|
||||||
|
|
||||||
|
let RAG_FULL_CONTEXT = false;
|
||||||
|
|
||||||
let enableGoogleDriveIntegration = false;
|
let enableGoogleDriveIntegration = false;
|
||||||
|
|
||||||
let OpenAIUrl = '';
|
let OpenAIUrl = '';
|
||||||
@@ -182,6 +183,7 @@
|
|||||||
max_size: fileMaxSize === '' ? null : fileMaxSize,
|
max_size: fileMaxSize === '' ? null : fileMaxSize,
|
||||||
max_count: fileMaxCount === '' ? null : fileMaxCount
|
max_count: fileMaxCount === '' ? null : fileMaxCount
|
||||||
},
|
},
|
||||||
|
RAG_FULL_CONTEXT: RAG_FULL_CONTEXT,
|
||||||
chunk: {
|
chunk: {
|
||||||
text_splitter: textSplitter,
|
text_splitter: textSplitter,
|
||||||
chunk_overlap: chunkOverlap,
|
chunk_overlap: chunkOverlap,
|
||||||
@@ -242,6 +244,8 @@
|
|||||||
chunkSize = res.chunk.chunk_size;
|
chunkSize = res.chunk.chunk_size;
|
||||||
chunkOverlap = res.chunk.chunk_overlap;
|
chunkOverlap = res.chunk.chunk_overlap;
|
||||||
|
|
||||||
|
RAG_FULL_CONTEXT = res.RAG_FULL_CONTEXT;
|
||||||
|
|
||||||
contentExtractionEngine = res.content_extraction.engine;
|
contentExtractionEngine = res.content_extraction.engine;
|
||||||
tikaServerUrl = res.content_extraction.tika_server_url;
|
tikaServerUrl = res.content_extraction.tika_server_url;
|
||||||
showTikaServerUrl = contentExtractionEngine === 'tika';
|
showTikaServerUrl = contentExtractionEngine === 'tika';
|
||||||
@@ -296,7 +300,7 @@
|
|||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model 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 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
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={embeddingEngine}
|
bind:value={embeddingEngine}
|
||||||
placeholder="Select an embedding model engine"
|
placeholder="Select an embedding model engine"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
@@ -319,7 +323,7 @@
|
|||||||
{#if embeddingEngine === 'openai'}
|
{#if embeddingEngine === 'openai'}
|
||||||
<div class="my-0.5 flex gap-2 pr-2">
|
<div class="my-0.5 flex gap-2 pr-2">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
|
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={OpenAIUrl}
|
bind:value={OpenAIUrl}
|
||||||
required
|
required
|
||||||
@@ -330,7 +334,7 @@
|
|||||||
{:else if embeddingEngine === 'ollama'}
|
{:else if embeddingEngine === 'ollama'}
|
||||||
<div class="my-0.5 flex gap-2 pr-2">
|
<div class="my-0.5 flex gap-2 pr-2">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
|
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={OllamaUrl}
|
bind:value={OllamaUrl}
|
||||||
required
|
required
|
||||||
@@ -375,7 +379,7 @@
|
|||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
toggleHybridSearch();
|
toggleHybridSearch();
|
||||||
}}
|
}}
|
||||||
@@ -388,9 +392,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Tooltip
|
||||||
|
content={RAG_FULL_CONTEXT
|
||||||
|
? 'Inject entire contents as context for comprehensive processing, this is recommended for complex queries.'
|
||||||
|
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||||
|
>
|
||||||
|
<Switch bind:state={RAG_FULL_CONTEXT} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="dark:border-gray-850" />
|
<hr class="border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="space-y-2" />
|
<div class="space-y-2" />
|
||||||
<div>
|
<div>
|
||||||
@@ -400,7 +417,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"
|
||||||
bind:value={embeddingModel}
|
bind:value={embeddingModel}
|
||||||
placeholder={$i18n.t('Set embedding model')}
|
placeholder={$i18n.t('Set embedding model')}
|
||||||
required
|
required
|
||||||
@@ -411,7 +428,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 embedding model (e.g. {{model}})', {
|
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
|
||||||
model: embeddingModel.slice(-40)
|
model: embeddingModel.slice(-40)
|
||||||
})}
|
})}
|
||||||
@@ -490,7 +507,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 reranking model (e.g. {{model}})', {
|
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
|
||||||
model: 'BAAI/bge-reranker-v2-m3'
|
model: 'BAAI/bge-reranker-v2-m3'
|
||||||
})}
|
})}
|
||||||
@@ -555,7 +572,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
|
<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
|
||||||
@@ -564,7 +581,7 @@
|
|||||||
<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div>
|
<div class="self-center text-xs font-medium">{$i18n.t('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 rounded px-2 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||||
bind:value={contentExtractionEngine}
|
bind:value={contentExtractionEngine}
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
showTikaServerUrl = e.target.value === 'tika';
|
showTikaServerUrl = e.target.value === 'tika';
|
||||||
@@ -580,7 +597,7 @@
|
|||||||
<div class="flex w-full mt-1">
|
<div class="flex w-full mt-1">
|
||||||
<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('Enter Tika Server URL')}
|
placeholder={$i18n.t('Enter Tika Server URL')}
|
||||||
bind:value={tikaServerUrl}
|
bind:value={tikaServerUrl}
|
||||||
/>
|
/>
|
||||||
@@ -589,7 +606,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
|
<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
|
||||||
|
|
||||||
@@ -602,7 +619,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
|
<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
|
||||||
@@ -613,7 +630,7 @@
|
|||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Top K')}
|
placeholder={$i18n.t('Enter Top K')}
|
||||||
bind:value={querySettings.k}
|
bind:value={querySettings.k}
|
||||||
@@ -631,7 +648,7 @@
|
|||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
placeholder={$i18n.t('Enter Score')}
|
placeholder={$i18n.t('Enter Score')}
|
||||||
@@ -667,7 +684,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
||||||
@@ -676,7 +693,7 @@
|
|||||||
<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
|
<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||||
bind:value={textSplitter}
|
bind:value={textSplitter}
|
||||||
>
|
>
|
||||||
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
|
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
|
||||||
@@ -692,7 +709,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Chunk Size')}
|
placeholder={$i18n.t('Enter Chunk Size')}
|
||||||
bind:value={chunkSize}
|
bind:value={chunkSize}
|
||||||
@@ -709,7 +726,7 @@
|
|||||||
|
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Enter Chunk Overlap')}
|
placeholder={$i18n.t('Enter Chunk Overlap')}
|
||||||
bind:value={chunkOverlap}
|
bind:value={chunkOverlap}
|
||||||
@@ -731,7 +748,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
|
<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
|
||||||
@@ -750,7 +767,7 @@
|
|||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Leave empty for unlimited')}
|
placeholder={$i18n.t('Leave empty for unlimited')}
|
||||||
bind:value={fileMaxSize}
|
bind:value={fileMaxSize}
|
||||||
@@ -773,7 +790,7 @@
|
|||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={$i18n.t('Leave empty for unlimited')}
|
placeholder={$i18n.t('Leave empty for unlimited')}
|
||||||
bind:value={fileMaxCount}
|
bind:value={fileMaxCount}
|
||||||
@@ -786,7 +803,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -261,6 +261,9 @@
|
|||||||
} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
|
} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
|
||||||
toast.error($i18n.t('OpenAI API Key is required.'));
|
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||||
config.enabled = false;
|
config.enabled = false;
|
||||||
|
} else if (config.engine === 'gemini' && config.gemini.GEMINI_API_KEY === '') {
|
||||||
|
toast.error($i18n.t('Gemini API Key is required.'));
|
||||||
|
config.enabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +287,7 @@
|
|||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation 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={config.engine}
|
bind:value={config.engine}
|
||||||
placeholder={$i18n.t('Select Engine')}
|
placeholder={$i18n.t('Select Engine')}
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
@@ -294,11 +297,12 @@
|
|||||||
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
||||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||||
<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
|
<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
|
||||||
|
<option value="gemini">{$i18n.t('Gemini')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
|
{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
|
||||||
@@ -307,7 +311,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('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
|
bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
|
||||||
/>
|
/>
|
||||||
@@ -386,7 +390,7 @@
|
|||||||
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
|
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
|
||||||
<input
|
<input
|
||||||
list="sampler-list"
|
list="sampler-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"
|
||||||
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
|
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
|
||||||
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
|
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
|
||||||
/>
|
/>
|
||||||
@@ -408,7 +412,7 @@
|
|||||||
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
|
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
|
||||||
<input
|
<input
|
||||||
list="scheduler-list"
|
list="scheduler-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"
|
||||||
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
|
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
|
||||||
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
|
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
|
||||||
/>
|
/>
|
||||||
@@ -429,7 +433,7 @@
|
|||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
|
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
|
||||||
<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('Enter CFG Scale (e.g. 7.0)')}
|
placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
|
||||||
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
|
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
|
||||||
/>
|
/>
|
||||||
@@ -443,7 +447,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('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
bind:value={config.comfyui.COMFYUI_BASE_URL}
|
bind:value={config.comfyui.COMFYUI_BASE_URL}
|
||||||
/>
|
/>
|
||||||
@@ -497,7 +501,7 @@
|
|||||||
|
|
||||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||||
<textarea
|
<textarea
|
||||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none disabled:text-gray-600 resize-none"
|
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
|
||||||
rows="10"
|
rows="10"
|
||||||
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
||||||
required
|
required
|
||||||
@@ -525,7 +529,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
document.getElementById('upload-comfyui-workflow-input')?.click();
|
document.getElementById('upload-comfyui-workflow-input')?.click();
|
||||||
@@ -548,7 +552,7 @@
|
|||||||
<div class="text-xs flex flex-col gap-1.5">
|
<div class="text-xs flex flex-col gap-1.5">
|
||||||
{#each requiredWorkflowNodes as node}
|
{#each requiredWorkflowNodes as node}
|
||||||
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<div
|
<div
|
||||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
||||||
>
|
>
|
||||||
@@ -558,7 +562,7 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
||||||
<input
|
<input
|
||||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
|
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
bind:value={node.key}
|
bind:value={node.key}
|
||||||
required
|
required
|
||||||
@@ -572,7 +576,7 @@
|
|||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
|
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
|
||||||
placeholder="Node Ids"
|
placeholder="Node Ids"
|
||||||
bind:value={node.node_ids}
|
bind:value={node.node_ids}
|
||||||
/>
|
/>
|
||||||
@@ -593,7 +597,7 @@
|
|||||||
|
|
||||||
<div class="flex gap-2 mb-1">
|
<div class="flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full text-sm bg-transparent outline-none"
|
class="flex-1 w-full text-sm bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={config.openai.OPENAI_API_BASE_URL}
|
bind:value={config.openai.OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
@@ -605,11 +609,29 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if config?.engine === 'gemini'}
|
||||||
|
<div>
|
||||||
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
class="flex-1 w-full text-sm bg-transparent outline-none"
|
||||||
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={config.gemini.GEMINI_API_BASE_URL}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
|
bind:value={config.gemini.GEMINI_API_KEY}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if config?.enabled}
|
{#if config?.enabled}
|
||||||
<hr class=" dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||||
@@ -620,7 +642,7 @@
|
|||||||
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
|
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
|
||||||
<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={imageGenerationConfig.MODEL}
|
bind:value={imageGenerationConfig.MODEL}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
required
|
required
|
||||||
@@ -644,7 +666,7 @@
|
|||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
|
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
|
||||||
<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('Enter Image Size (e.g. 512x512)')}
|
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||||
bind:value={imageGenerationConfig.IMAGE_SIZE}
|
bind:value={imageGenerationConfig.IMAGE_SIZE}
|
||||||
required
|
required
|
||||||
@@ -660,7 +682,7 @@
|
|||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
|
<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
|
||||||
<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('Enter Number of Steps (e.g. 50)')}
|
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||||
bind:value={imageGenerationConfig.IMAGE_STEPS}
|
bind:value={imageGenerationConfig.IMAGE_STEPS}
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
let taskConfig = {
|
let taskConfig = {
|
||||||
TASK_MODEL: '',
|
TASK_MODEL: '',
|
||||||
TASK_MODEL_EXTERNAL: '',
|
TASK_MODEL_EXTERNAL: '',
|
||||||
|
ENABLE_TITLE_GENERATION: true,
|
||||||
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '',
|
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '',
|
||||||
ENABLE_AUTOCOMPLETE_GENERATION: true,
|
ENABLE_AUTOCOMPLETE_GENERATION: true,
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
taskConfig = await getTaskConfig(localStorage.token);
|
taskConfig = await getTaskConfig(localStorage.token);
|
||||||
|
|
||||||
promptSuggestions = $config?.default_prompt_suggestions;
|
promptSuggestions = $config?.default_prompt_suggestions ?? [];
|
||||||
banners = await getBanners(localStorage.token);
|
banners = await getBanners(localStorage.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,9 +69,13 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||||
<div>
|
<div class="mb-3.5">
|
||||||
<div class=" mb-2.5 text-sm font-medium flex items-center">
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
|
||||||
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class=" mb-1 font-medium flex items-center">
|
||||||
|
<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
||||||
@@ -92,11 +97,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
|
<div class=" mb-2.5 flex w-full gap-2">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
||||||
<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={taskConfig.TASK_MODEL}
|
bind:value={taskConfig.TASK_MODEL}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
@@ -112,7 +118,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
||||||
<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={taskConfig.TASK_MODEL_EXTERNAL}
|
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
@@ -126,72 +132,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<Textarea
|
|
||||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
|
||||||
placeholder={$i18n.t(
|
|
||||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<Textarea
|
|
||||||
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
|
|
||||||
placeholder={$i18n.t(
|
|
||||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Autocomplete Generation')}
|
{$i18n.t('Title Generation')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
|
<Switch bind:state={taskConfig.ENABLE_TITLE_GENERATION} />
|
||||||
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
|
{#if taskConfig.ENABLE_TITLE_GENERATION}
|
||||||
<div class="mt-3">
|
<div class="mb-2.5">
|
||||||
<div class=" mb-2.5 text-xs font-medium">
|
<div class=" mb-1 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||||
{$i18n.t('Autocomplete Generation Input Max Length')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t('Character limit for autocomplete generation input')}
|
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<Textarea
|
||||||
class="w-full outline-none bg-transparent"
|
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||||
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
|
placeholder={$i18n.t(
|
||||||
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
|
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Tags Generation')}
|
{$i18n.t('Tags Generation')}
|
||||||
</div>
|
</div>
|
||||||
@@ -200,8 +167,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if taskConfig.ENABLE_TAGS_GENERATION}
|
{#if taskConfig.ENABLE_TAGS_GENERATION}
|
||||||
<div class="mt-3">
|
<div class="mb-2.5">
|
||||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
|
<div class=" mb-1 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||||
@@ -217,9 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Retrieval Query Generation')}
|
{$i18n.t('Retrieval Query Generation')}
|
||||||
</div>
|
</div>
|
||||||
@@ -227,7 +192,7 @@
|
|||||||
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
|
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3 flex w-full items-center justify-between">
|
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Web Search Query Generation')}
|
{$i18n.t('Web Search Query Generation')}
|
||||||
</div>
|
</div>
|
||||||
@@ -235,8 +200,8 @@
|
|||||||
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
|
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="">
|
<div class="mb-2.5">
|
||||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
|
<div class=" mb-1 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||||
@@ -250,131 +215,96 @@
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Autocomplete Generation')}
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<Textarea
|
|
||||||
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
|
|
||||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
|
||||||
|
|
||||||
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-sm font-semibold">
|
|
||||||
{$i18n.t('Banners')}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
|
||||||
type="button"
|
</Tooltip>
|
||||||
on:click={() => {
|
|
||||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
|
||||||
banners = [
|
|
||||||
...banners,
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
type: '',
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
dismissible: true,
|
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-1">
|
|
||||||
{#each banners as banner, bannerIdx}
|
|
||||||
<div class=" flex justify-between">
|
|
||||||
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
|
||||||
<select
|
|
||||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
|
||||||
bind:value={banner.type}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
{#if banner.type == ''}
|
|
||||||
<option value="" selected disabled class="text-gray-900"
|
|
||||||
>{$i18n.t('Type')}</option
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
|
||||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
|
||||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
|
||||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input
|
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
|
||||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
<div class="mb-2.5">
|
||||||
placeholder={$i18n.t('Content')}
|
<div class=" mb-1 text-xs font-medium">
|
||||||
bind:value={banner.content}
|
{$i18n.t('Autocomplete Generation Input Max Length')}
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="relative top-1.5 -left-2">
|
|
||||||
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
|
||||||
<Switch bind:state={banner.dismissible} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-2"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
banners.splice(bannerIdx, 1);
|
|
||||||
banners = banners;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('Character limit for autocomplete generation input')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full outline-hidden bg-transparent"
|
||||||
|
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
|
||||||
|
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mb-2.5">
|
||||||
|
<div class=" mb-1 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
|
||||||
|
placeholder={$i18n.t(
|
||||||
|
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5">
|
||||||
|
<div class=" mb-1 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
|
||||||
|
placeholder={$i18n.t(
|
||||||
|
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $user.role === 'admin'}
|
<div class="mb-3.5">
|
||||||
<div class=" space-y-3">
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
|
||||||
<div class="flex w-full justify-between mb-2">
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class=" {banners.length > 0 ? ' mb-3' : ''}">
|
||||||
|
<div class="mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-semibold">
|
<div class=" self-center text-sm font-semibold">
|
||||||
{$i18n.t('Default Prompt Suggestions')}
|
{$i18n.t('Banners')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
banners = [
|
||||||
|
...banners,
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
dismissible: true,
|
||||||
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -390,40 +320,48 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
|
||||||
{#each promptSuggestions as prompt, promptIdx}
|
|
||||||
<div
|
|
||||||
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col flex-1 pl-1">
|
|
||||||
<div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
|
|
||||||
<input
|
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
|
||||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
|
||||||
bind:value={prompt.title[0]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
<div class=" flex flex-col space-y-1">
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
{#each banners as banner, bannerIdx}
|
||||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
<div class=" flex justify-between">
|
||||||
bind:value={prompt.title[1]}
|
<div
|
||||||
/>
|
class="flex flex-row flex-1 border rounded-xl border-gray-100 dark:border-gray-850"
|
||||||
</div>
|
>
|
||||||
|
<select
|
||||||
|
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-hidden"
|
||||||
|
bind:value={banner.type}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{#if banner.type == ''}
|
||||||
|
<option value="" selected disabled class="text-gray-900"
|
||||||
|
>{$i18n.t('Type')}</option
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||||
|
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||||
|
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||||
|
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<textarea
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
|
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
placeholder={$i18n.t('Content')}
|
||||||
rows="3"
|
bind:value={banner.content}
|
||||||
bind:value={prompt.content}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="relative top-1.5 -left-2">
|
||||||
|
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
||||||
|
<Switch bind:state={banner.dismissible} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-3"
|
class="px-2"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
promptSuggestions.splice(promptIdx, 1);
|
banners.splice(bannerIdx, 1);
|
||||||
promptSuggestions = promptSuggestions;
|
banners = banners;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -440,14 +378,97 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if promptSuggestions.length > 0}
|
|
||||||
<div class="text-xs text-left w-full mt-2">
|
|
||||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
{#if $user.role === 'admin'}
|
||||||
|
<div class=" space-y-3">
|
||||||
|
<div class="flex w-full justify-between mb-2">
|
||||||
|
<div class=" self-center text-sm font-semibold">
|
||||||
|
{$i18n.t('Default Prompt Suggestions')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||||
|
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
||||||
|
{#each promptSuggestions as prompt, promptIdx}
|
||||||
|
<div
|
||||||
|
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col flex-1 pl-1">
|
||||||
|
<div class="flex border-b border-gray-100 dark:border-gray-850 w-full">
|
||||||
|
<input
|
||||||
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
|
||||||
|
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||||
|
bind:value={prompt.title[0]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
|
||||||
|
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||||
|
bind:value={prompt.title[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850 resize-none"
|
||||||
|
placeholder={$i18n.t(
|
||||||
|
'Prompt (e.g. Tell me a fun fact about the Roman Empire)'
|
||||||
|
)}
|
||||||
|
rows="3"
|
||||||
|
bind:value={prompt.content}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-3"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
promptSuggestions.splice(promptIdx, 1);
|
||||||
|
promptSuggestions = promptSuggestions;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if promptSuggestions.length > 0}
|
||||||
|
<div class="text-xs text-left w-full mt-2">
|
||||||
|
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end text-sm font-medium">
|
<div class="flex justify-end text-sm font-medium">
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
<Search className="size-3.5" />
|
<Search className="size-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
|
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
bind:value={searchValue}
|
bind:value={searchValue}
|
||||||
placeholder={$i18n.t('Search Models')}
|
placeholder={$i18n.t('Search Models')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -165,7 +165,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>
|
||||||
@@ -186,7 +186,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={() => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{#if ollamaConfig}
|
{#if ollamaConfig}
|
||||||
<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
|
<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
|
||||||
<select
|
<select
|
||||||
class="w-full py-2 px-4 text-sm outline-none bg-transparent"
|
class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
|
||||||
bind:value={selectedUrlIdx}
|
bind:value={selectedUrlIdx}
|
||||||
placeholder={$i18n.t('Select an Ollama instance')}
|
placeholder={$i18n.t('Select an Ollama instance')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -598,7 +598,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('Enter model tag (e.g. {{modelTag}})', {
|
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||||
modelTag: 'mistral:7b'
|
modelTag: 'mistral:7b'
|
||||||
})}
|
})}
|
||||||
@@ -740,7 +740,7 @@
|
|||||||
class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
|
class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
class="w-full py-2 px-4 text-sm outline-none bg-transparent"
|
class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
|
||||||
bind:value={deleteModelTag}
|
bind:value={deleteModelTag}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
@@ -781,7 +781,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2 flex flex-col gap-2">
|
<div class="flex-1 mr-2 flex flex-col gap-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('Enter model tag (e.g. {{modelTag}})', {
|
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||||
modelTag: 'my-modelfile'
|
modelTag: 'my-modelfile'
|
||||||
})}
|
})}
|
||||||
@@ -791,7 +791,7 @@
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={createModelObject}
|
bind:value={createModelObject}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none scrollbar-hidden"
|
||||||
rows="6"
|
rows="6"
|
||||||
placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
|
placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
|
||||||
disabled={createModelLoading}
|
disabled={createModelLoading}
|
||||||
@@ -870,7 +870,7 @@
|
|||||||
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
|
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (modelUploadMode === 'file') {
|
if (modelUploadMode === 'file') {
|
||||||
modelUploadMode = 'url';
|
modelUploadMode = 'url';
|
||||||
@@ -922,7 +922,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden {modelFileUrl !==
|
||||||
''
|
''
|
||||||
? 'mr-2'
|
? 'mr-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
@@ -998,7 +998,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={modelFileContent}
|
bind:value={modelFileContent}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none"
|
||||||
rows="6"
|
rows="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<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={selectedPipelinesUrlIdx}
|
bind:value={selectedPipelinesUrlIdx}
|
||||||
placeholder={$i18n.t('Select a pipeline url')}
|
placeholder={$i18n.t('Select a pipeline url')}
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
document.getElementById('pipelines-upload-input')?.click();
|
document.getElementById('pipelines-upload-input')?.click();
|
||||||
@@ -348,7 +348,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('Enter Github Raw URL')}
|
placeholder={$i18n.t('Enter Github Raw URL')}
|
||||||
bind:value={pipelineDownloadUrl}
|
bind:value={pipelineDownloadUrl}
|
||||||
/>
|
/>
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
|
||||||
|
|
||||||
{#if pipelines !== null}
|
{#if pipelines !== null}
|
||||||
{#if pipelines.length > 0}
|
{#if pipelines.length > 0}
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<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={selectedPipelineIdx}
|
bind:value={selectedPipelineIdx}
|
||||||
placeholder={$i18n.t('Select a pipeline')}
|
placeholder={$i18n.t('Select a pipeline')}
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
@@ -482,7 +482,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
valves[property] = (valves[property] ?? null) === null ? '' : null;
|
valves[property] = (valves[property] ?? null) === null ? '' : null;
|
||||||
@@ -502,7 +502,7 @@
|
|||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
{#if valves_spec.properties[property]?.enum ?? null}
|
{#if valves_spec.properties[property]?.enum ?? null}
|
||||||
<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={valves[property]}
|
bind:value={valves[property]}
|
||||||
>
|
>
|
||||||
{#each valves_spec.properties[property].enum as option}
|
{#each valves_spec.properties[property].enum as option}
|
||||||
@@ -523,7 +523,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={valves_spec.properties[property].title}
|
placeholder={valves_spec.properties[property].title}
|
||||||
bind:value={valves[property]}
|
bind:value={valves[property]}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
'serper',
|
'serper',
|
||||||
'serply',
|
'serply',
|
||||||
'searchapi',
|
'searchapi',
|
||||||
|
'serpapi',
|
||||||
'duckduckgo',
|
'duckduckgo',
|
||||||
'tavily',
|
'tavily',
|
||||||
'jina',
|
'jina',
|
||||||
@@ -102,7 +104,7 @@
|
|||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search 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 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
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={webConfig.search.engine}
|
bind:value={webConfig.search.engine}
|
||||||
placeholder={$i18n.t('Select a engine')}
|
placeholder={$i18n.t('Select a engine')}
|
||||||
required
|
required
|
||||||
@@ -115,6 +117,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Tooltip
|
||||||
|
content={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT
|
||||||
|
? 'Inject the entire web results as context for comprehensive processing, this is recommended for complex queries.'
|
||||||
|
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||||
|
>
|
||||||
|
<Switch bind:state={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if webConfig.search.engine !== ''}
|
{#if webConfig.search.engine !== ''}
|
||||||
<div class="mt-1.5">
|
<div class="mt-1.5">
|
||||||
{#if webConfig.search.engine === 'searxng'}
|
{#if webConfig.search.engine === 'searxng'}
|
||||||
@@ -126,7 +141,7 @@
|
|||||||
<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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||||
bind:value={webConfig.search.searxng_query_url}
|
bind:value={webConfig.search.searxng_query_url}
|
||||||
@@ -154,7 +169,7 @@
|
|||||||
<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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
||||||
bind:value={webConfig.search.google_pse_engine_id}
|
bind:value={webConfig.search.google_pse_engine_id}
|
||||||
@@ -259,7 +274,7 @@
|
|||||||
<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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter SearchApi Engine')}
|
placeholder={$i18n.t('Enter SearchApi Engine')}
|
||||||
bind:value={webConfig.search.searchapi_engine}
|
bind:value={webConfig.search.searchapi_engine}
|
||||||
@@ -268,6 +283,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'serpapi'}
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('SerpApi API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('Enter SerpApi API Key')}
|
||||||
|
bind:value={webConfig.search.serpapi_api_key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1.5">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('SerpApi Engine')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1">
|
||||||
|
<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={$i18n.t('Enter SerpApi Engine')}
|
||||||
|
bind:value={webConfig.search.serpapi_engine}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if webConfig.search.engine === 'tavily'}
|
{:else if webConfig.search.engine === 'tavily'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
@@ -310,7 +353,7 @@
|
|||||||
<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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
|
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
|
||||||
bind:value={webConfig.search.bing_search_v7_endpoint}
|
bind:value={webConfig.search.bing_search_v7_endpoint}
|
||||||
@@ -342,7 +385,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-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('Search Result Count')}
|
placeholder={$i18n.t('Search Result Count')}
|
||||||
bind:value={webConfig.search.result_count}
|
bind:value={webConfig.search.result_count}
|
||||||
required
|
required
|
||||||
@@ -355,7 +398,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-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('Concurrent Requests')}
|
placeholder={$i18n.t('Concurrent Requests')}
|
||||||
bind:value={webConfig.search.concurrent_requests}
|
bind:value={webConfig.search.concurrent_requests}
|
||||||
required
|
required
|
||||||
@@ -369,7 +412,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-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(
|
placeholder={$i18n.t(
|
||||||
'Enter domains separated by commas (e.g., example.com,site.org)'
|
'Enter domains separated by commas (e.g., example.com,site.org)'
|
||||||
)}
|
)}
|
||||||
@@ -379,7 +422,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</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 text-sm font-medium">
|
<div class=" mb-1 text-sm font-medium">
|
||||||
@@ -393,14 +436,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
webConfig.web_loader_ssl_verification = !webConfig.web_loader_ssl_verification;
|
webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION =
|
||||||
|
!webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION;
|
||||||
submitHandler();
|
submitHandler();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if webConfig.web_loader_ssl_verification === false}
|
{#if webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION === false}
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
@@ -418,7 +462,7 @@
|
|||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter language codes')}
|
placeholder={$i18n.t('Enter language codes')}
|
||||||
bind:value={youtubeLanguage}
|
bind:value={youtubeLanguage}
|
||||||
@@ -433,7 +477,7 @@
|
|||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
|
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-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"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
|
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
|
||||||
bind:value={youtubeProxyUrl}
|
bind:value={youtubeProxyUrl}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</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={search}
|
bind:value={search}
|
||||||
placeholder={$i18n.t('Search')}
|
placeholder={$i18n.t('Search')}
|
||||||
/>
|
/>
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<div class="w-full"></div>
|
<div class="w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="mt-1.5 border-gray-50 dark:border-gray-850" />
|
<hr class="mt-1.5 border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
{#each filteredGroups as group}
|
{#each filteredGroups as group}
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="mb-2 border-gray-50 dark:border-gray-850" />
|
<hr class="mb-2 border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<GroupModal
|
<GroupModal
|
||||||
bind:show={showDefaultPermissionsModal}
|
bind:show={showDefaultPermissionsModal}
|
||||||
|
|||||||
@@ -78,7 +78,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('Group Name')}
|
placeholder={$i18n.t('Group Name')}
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Textarea
|
<Textarea
|
||||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
|
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
|
||||||
rows={2}
|
rows={2}
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
placeholder={$i18n.t('Group Description')}
|
placeholder={$i18n.t('Group Description')}
|
||||||
|
|||||||
@@ -16,7 +16,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('Group Name')}
|
placeholder={$i18n.t('Group Name')}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<div class="text-gray-500">#</div>
|
<div class="text-gray-500">#</div>
|
||||||
|
|
||||||
<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={color}
|
bind:value={color}
|
||||||
placeholder={$i18n.t('Hex Color')}
|
placeholder={$i18n.t('Hex Color')}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Textarea
|
<Textarea
|
||||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
|
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
|
||||||
rows={4}
|
rows={4}
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
placeholder={$i18n.t('Group Description')}
|
placeholder={$i18n.t('Group Description')}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
<div class=" text-sm flex-1 rounded-lg">
|
<div class=" text-sm flex-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={() => {
|
||||||
@@ -102,7 +102,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>
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<select
|
<select
|
||||||
class="w-full bg-transparent outline-none py-0.5 text-sm"
|
class="w-full bg-transparent outline-hidden py-0.5 text-sm"
|
||||||
bind:value={permissions.model.default_id}
|
bind:value={permissions.model.default_id}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
>
|
>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> -->
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> -->
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent"
|
class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
placeholder={$i18n.t('Search')}
|
placeholder={$i18n.t('Search')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -85,8 +85,9 @@
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let name = user.name.toLowerCase();
|
let name = user.name.toLowerCase();
|
||||||
|
let email = user.email.toLowerCase();
|
||||||
const query = search.toLowerCase();
|
const query = search.toLowerCase();
|
||||||
return name.includes(query);
|
return name.includes(query) || email.includes(query);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@@ -149,7 +150,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</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={search}
|
bind:value={search}
|
||||||
placeholder={$i18n.t('Search')}
|
placeholder={$i18n.t('Search')}
|
||||||
/>
|
/>
|
||||||
@@ -171,9 +172,11 @@
|
|||||||
</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"
|
||||||
|
>
|
||||||
<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"
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-none"
|
class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-hidden"
|
||||||
bind:value={_user.role}
|
bind:value={_user.role}
|
||||||
placeholder={$i18n.t('Enter Your Role')}
|
placeholder={$i18n.t('Enter Your Role')}
|
||||||
required
|
required
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={_user.name}
|
bind:value={_user.name}
|
||||||
placeholder={$i18n.t('Enter Your Full Name')}
|
placeholder={$i18n.t('Enter Your Full Name')}
|
||||||
@@ -208,14 +208,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2.5 w-full" />
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={_user.email}
|
bind:value={_user.email}
|
||||||
placeholder={$i18n.t('Enter Your Email')}
|
placeholder={$i18n.t('Enter Your Email')}
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
type="password"
|
type="password"
|
||||||
bind:value={_user.password}
|
bind:value={_user.password}
|
||||||
placeholder={$i18n.t('Enter Your Password')}
|
placeholder={$i18n.t('Enter Your Password')}
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
document.getElementById('upload-user-csv-input')?.click();
|
document.getElementById('upload-user-csv-input')?.click();
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-800" />
|
<hr class="border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
|
||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
|
||||||
|
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={_user.email}
|
bind:value={_user.email}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={_user.name}
|
bind:value={_user.name}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
|
||||||
type="password"
|
type="password"
|
||||||
bind:value={_user.password}
|
bind:value={_user.password}
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<div class="relative overflow-x-auto">
|
<div class="relative overflow-x-auto">
|
||||||
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
||||||
<thead
|
<thead
|
||||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
|
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-850"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
|
|||||||
@@ -281,7 +281,7 @@
|
|||||||
<PaneResizer
|
<PaneResizer
|
||||||
class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
|
class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
|
||||||
>
|
>
|
||||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
||||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||||
</div>
|
</div>
|
||||||
</PaneResizer>
|
</PaneResizer>
|
||||||
|
|||||||
@@ -103,7 +103,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
if (
|
||||||
|
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||||
|
) {
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
@@ -455,7 +457,7 @@
|
|||||||
|
|
||||||
<div class="px-2.5">
|
<div class="px-2.5">
|
||||||
<div
|
<div
|
||||||
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
||||||
>
|
>
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
bind:value={content}
|
bind:value={content}
|
||||||
@@ -513,7 +515,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
|
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="More"
|
aria-label="More"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[200px] rounded-xl px-1 py-1 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-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||||
sideOffset={15}
|
sideOffset={15}
|
||||||
alignOffset={-8}
|
alignOffset={-8}
|
||||||
side="top"
|
side="top"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800"
|
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
|
||||||
>
|
>
|
||||||
<ReactionPicker
|
<ReactionPicker
|
||||||
onClose={() => (showButtons = false)}
|
onClose={() => (showButtons = false)}
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
dir={$settings.chatDirection}
|
dir={$settings.chatDirection}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
||||||
>
|
>
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
<ProfilePreview user={message.user}>
|
<ProfilePreview user={message.user}>
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
|
|
||||||
{#if message.created_at}
|
{#if message.created_at}
|
||||||
<div
|
<div
|
||||||
class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
|
class="mt-1.5 flex shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
|
||||||
>
|
>
|
||||||
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
|
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
|
||||||
{dayjs(message.created_at / 1000000).format('HH:mm')}
|
{dayjs(message.created_at / 1000000).format('HH:mm')}
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
{#if edit}
|
{#if edit}
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<Textarea
|
<Textarea
|
||||||
className=" bg-transparent outline-none w-full resize-none"
|
className=" bg-transparent outline-hidden w-full resize-none"
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
onKeydown={(e) => {
|
onKeydown={(e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<slot name="content">
|
<slot name="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="max-w-full w-[240px] rounded-lg z-[9999] bg-white dark:bg-black dark:text-white shadow-lg"
|
class="max-w-full w-[240px] rounded-lg z-9999 bg-white dark:bg-black dark:text-white shadow-lg"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
{side}
|
{side}
|
||||||
{align}
|
{align}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
|
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-9999 shadow-lg dark:text-white"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
{side}
|
{side}
|
||||||
{align}
|
{align}
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
<div class="mb-1 px-3 pt-2 pb-2">
|
<div class="mb-1 px-3 pt-2 pb-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full text-sm bg-transparent outline-none"
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
placeholder="Search all emojis"
|
placeholder="Search all emojis"
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
||||||
<div
|
<div
|
||||||
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
|
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
|
||||||
|
|||||||
@@ -1241,7 +1241,7 @@
|
|||||||
// Response not done
|
// Response not done
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (messages.length != 0 && messages.at(-1).error) {
|
if (messages.length != 0 && messages.at(-1).error && !messages.at(-1).content) {
|
||||||
// Error in response
|
// Error in response
|
||||||
toast.error($i18n.t(`Oops! There was an error in the previous response.`));
|
toast.error($i18n.t(`Oops! There was an error in the previous response.`));
|
||||||
return;
|
return;
|
||||||
@@ -1896,7 +1896,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
|
class="absolute top-0 left-0 w-full h-full bg-linear-to-t from-white to-white/85 dark:from-gray-900 dark:to-gray-900/90 z-0"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@
|
|||||||
|
|
||||||
{#if $showControls}
|
{#if $showControls}
|
||||||
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
||||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
||||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||||
</div>
|
</div>
|
||||||
</PaneResizer>
|
</PaneResizer>
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
<div
|
<div
|
||||||
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
||||||
? ' '
|
? ' '
|
||||||
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||||
>
|
>
|
||||||
{#if $showCallOverlay}
|
{#if $showCallOverlay}
|
||||||
<div class="w-full h-full flex justify-center">
|
<div class="w-full h-full flex justify-center">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user