diff --git a/CHANGELOG.md b/CHANGELOG.md
index a11c2848e..2dcf4b3da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.6.3] - 2025-04-12
+
+### Added
+
+- 🧪 **Auto-Artifact Detection Toggle**: Automatically detects artifacts in results—but now you can disable this behavior under advanced settings for full control.
+- 🖼️ **Widescreen Mode for Shared Chats**: Shared link conversations now support widescreen layouts—perfect for presentations or easier review across wider displays.
+- 🔁 **Reindex Knowledge Files on Demand**: Admins can now trigger reindexing of all knowledge files after changing embeddings—ensuring immediate alignment with new models for optimal RAG performance.
+- 📄 **OpenAPI YAML Format Support**: External tools can now use YAML-format OpenAPI specs—making integration simpler for developers familiar with YAML-based configurations.
+- 💬 **Message Content Copy Behavior**: Copy action now excludes 'details' tags—streamlining clipboard content when sharing or pasting summaries elsewhere.
+- 🧭 **Sougou Web Search Integration**: New search engine option added—enhancing global relevance and diversity of search sources for multilingual users.
+- 🧰 **Frontend Web Loader Engine Configuration**: Admins can now set preferred web loader engine for RAG workflows directly from the frontend—offering more control across setups.
+- 👥 **Multi-Model Chat Permission Control**: Admins can manage access to multi-model chats per user group—allowing tighter governance in team environments.
+- 🧱 **Persistent Configuration Can Be Disabled**: New environment variable lets advanced users and hosts turn off persistent configs—ideal for volatile or stateless deployments.
+- 🧠 **Elixir Code Highlighting Support**: Elixir syntax is now beautifully rendered in code blocks—perfect for developers using this language in AI or automation projects.
+- 🌐 **PWA External Manifest URL Support**: You can now define an external manifest.json—integrate Open WebUI seamlessly in managed or proxy-based PWA environments like Cloudflare Zero Trust.
+- 🧪 **Azure AI Speech-to-Text Provider Integration**: Easily transcribe large audio files (up to 200MB) with high accuracy using Microsoft's Azure STT—fully configurable in Audio Settings.
+- 🔏 **PKCE (Code Challenge Method) Support for OIDC**: Enhance your OIDC login security with Proof Key for Code Exchange—ideal for zero-trust and native client apps.
+- ✨ **General UI/UX Enhancements**: Numerous refinements across layout, styling, and tool interactions—reducing visual noise and improving overall usability across key workflows.
+- 🌍 **Translation Updates Across Multiple Languages**: Refined Catalan, Russian, Chinese (Simplified & Traditional), Hungarian, and Spanish translations for clearer navigation and instructions globally.
+
+### Fixed
+
+- 💥 **Chat Completion Error with Missing Models Resolved**: Fixed internal server error when referencing a model that doesn’t exist—ensuring graceful fallback and clear error guidance.
+- 🔧 **Correct Knowledge Base Citations Restored**: Citations generated by RAG workflows now show accurate references—ensuring verifiability in outputs from sourced content.
+- 🎙️ **Broken OGG/WebM Audio Upload Handling for OpenAI Fixed**: Uploading OGG or WebM files now converts properly to WAV before transcription—restoring accurate AI speech recognition workflows.
+- 🔐 **Tool Server 'Session' Authentication Restored**: Previously broken session auth on external tool servers is now fully functional—ensuring secure and seamless access to connected tools.
+- 🌐 **Folder-Based Chat Rename Now Updates Correctly**: Renaming chats in folders now reflects instantly everywhere—improving chat organization and clarity.
+- 📜 **KaTeX Overflow Displays Fixed**: Math expressions now stay neatly within message bounds—preserving layout consistency even with long formulas.
+- 🚫 **Stopping Ongoing Chat Fixed**: You can now return to an active (ongoing) chat and stop generation at any time—ensuring full control over sessions.
+- 🔧 **TOOL_SERVERS / TOOL_SERVER_CONNECTIONS Indexing Issue Fixed**: Fixed a mismatch between tool lists and their access paths—restoring full function and preventing confusion in tool management.
+- 🔐 **LDAP Login Handles Multiple Emails**: When LDAP returns multiple email attributes, the first valid one is now used—ensuring login success and account consistency.
+- 🧩 **Model Visibility Toggle Fix**: Toggling model visibility now works even for untouched models—letting admins smoothly manage user access across base models.
+- ⚙️ **Cross-Origin manifest.json Now Loads Properly**: Compatibility issues with Cloudflare Zero Trust (and others) resolved, allowing manifest.json to load behind authenticated proxies.
+
+### Changed
+
+- 🔒 **Default Access Scopes Set to Private for All Resources**: Models, tools, and knowledge are now private by default when created—ensuring better baseline security and visibility controls.
+- 🧱 **General Backend Refactoring for Stability**: Numerous invisible improvements enhance backend scalability, security, and maintainability—powering upcoming features with a stronger foundation.
+- 🧩 **Stable Dependency Upgrades**: Updated key platform libraries—Chromadb (0.6.3), pgvector (0.4.0), Azure Identity (1.21.0), and Youtube Transcript API (1.0.3)—for improved compatibility, functionality, and security.
+
## [0.6.2] - 2025-04-06
### Added
diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py
index d85be48da..ff386957c 100644
--- a/backend/open_webui/__init__.py
+++ b/backend/open_webui/__init__.py
@@ -73,8 +73,15 @@ def serve(
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
import open_webui.main # we need set environment variables before importing main
+ from open_webui.env import UVICORN_WORKERS # Import the workers setting
- uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
+ uvicorn.run(
+ open_webui.main.app,
+ host=host,
+ port=port,
+ forwarded_allow_ips="*",
+ workers=UVICORN_WORKERS,
+ )
@app.command()
diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py
index 8238f8a87..bd822d06d 100644
--- a/backend/open_webui/config.py
+++ b/backend/open_webui/config.py
@@ -201,6 +201,10 @@ def save_config(config):
T = TypeVar("T")
+ENABLE_PERSISTENT_CONFIG = (
+ os.environ.get("ENABLE_PERSISTENT_CONFIG", "True").lower() == "true"
+)
+
class PersistentConfig(Generic[T]):
def __init__(self, env_name: str, config_path: str, env_value: T):
@@ -208,7 +212,7 @@ class PersistentConfig(Generic[T]):
self.config_path = config_path
self.env_value = env_value
self.config_value = get_config_value(config_path)
- if self.config_value is not None:
+ if self.config_value is not None and ENABLE_PERSISTENT_CONFIG:
log.info(f"'{env_name}' loaded from the latest database entry")
self.value = self.config_value
else:
@@ -456,6 +460,12 @@ OAUTH_SCOPES = PersistentConfig(
os.environ.get("OAUTH_SCOPES", "openid email profile"),
)
+OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
+ "OAUTH_CODE_CHALLENGE_METHOD",
+ "oauth.oidc.code_challenge_method",
+ os.environ.get("OAUTH_CODE_CHALLENGE_METHOD", None),
+)
+
OAUTH_PROVIDER_NAME = PersistentConfig(
"OAUTH_PROVIDER_NAME",
"oauth.oidc.provider_name",
@@ -560,7 +570,7 @@ def load_oauth_providers():
name="microsoft",
client_id=MICROSOFT_CLIENT_ID.value,
client_secret=MICROSOFT_CLIENT_SECRET.value,
- server_metadata_url=f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
+ server_metadata_url=f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}",
client_kwargs={
"scope": MICROSOFT_OAUTH_SCOPE.value,
},
@@ -601,14 +611,27 @@ def load_oauth_providers():
):
def oidc_oauth_register(client):
+ client_kwargs = {
+ "scope": OAUTH_SCOPES.value,
+ }
+
+ if (
+ OAUTH_CODE_CHALLENGE_METHOD.value
+ and OAUTH_CODE_CHALLENGE_METHOD.value == "S256"
+ ):
+ client_kwargs["code_challenge_method"] = "S256"
+ elif OAUTH_CODE_CHALLENGE_METHOD.value:
+ raise Exception(
+ 'Code challenge methods other than "%s" not supported. Given: "%s"'
+ % ("S256", OAUTH_CODE_CHALLENGE_METHOD.value)
+ )
+
client.register(
name="oidc",
client_id=OAUTH_CLIENT_ID.value,
client_secret=OAUTH_CLIENT_SECRET.value,
server_metadata_url=OPENID_PROVIDER_URL.value,
- client_kwargs={
- "scope": OAUTH_SCOPES.value,
- },
+ client_kwargs=client_kwargs,
redirect_uri=OPENID_REDIRECT_URI.value,
)
@@ -1039,6 +1062,10 @@ USER_PERMISSIONS_CHAT_EDIT = (
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
)
+USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = (
+ os.environ.get("USER_PERMISSIONS_CHAT_MULTIPLE_MODELS", "True").lower() == "true"
+)
+
USER_PERMISSIONS_CHAT_TEMPORARY = (
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
)
@@ -1048,6 +1075,7 @@ USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = (
== "true"
)
+
USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = (
os.environ.get("USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS", "False").lower()
== "true"
@@ -1086,6 +1114,7 @@ DEFAULT_USER_PERMISSIONS = {
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT,
+ "multiple_models": USER_PERMISSIONS_CHAT_MULTIPLE_MODELS,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
"temporary_enforced": USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED,
},
@@ -1806,12 +1835,6 @@ RAG_FILE_MAX_SIZE = PersistentConfig(
),
)
-ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
- "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
- "rag.enable_web_loader_ssl_verification",
- os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
-)
-
RAG_EMBEDDING_ENGINE = PersistentConfig(
"RAG_EMBEDDING_ENGINE",
"rag.embedding_engine",
@@ -1976,16 +1999,20 @@ YOUTUBE_LOADER_PROXY_URL = PersistentConfig(
)
-ENABLE_RAG_WEB_SEARCH = PersistentConfig(
- "ENABLE_RAG_WEB_SEARCH",
+####################################
+# Web Search (RAG)
+####################################
+
+ENABLE_WEB_SEARCH = PersistentConfig(
+ "ENABLE_WEB_SEARCH",
"rag.web.search.enable",
- os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
+ os.getenv("ENABLE_WEB_SEARCH", "False").lower() == "true",
)
-RAG_WEB_SEARCH_ENGINE = PersistentConfig(
- "RAG_WEB_SEARCH_ENGINE",
+WEB_SEARCH_ENGINE = PersistentConfig(
+ "WEB_SEARCH_ENGINE",
"rag.web.search.engine",
- os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
+ os.getenv("WEB_SEARCH_ENGINE", ""),
)
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
@@ -1994,10 +2021,18 @@ BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
)
+
+WEB_SEARCH_RESULT_COUNT = PersistentConfig(
+ "WEB_SEARCH_RESULT_COUNT",
+ "rag.web.search.result_count",
+ int(os.getenv("WEB_SEARCH_RESULT_COUNT", "3")),
+)
+
+
# 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.
-RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
- "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
+WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
+ "WEB_SEARCH_DOMAIN_FILTER_LIST",
"rag.web.search.domain.filter_list",
[
# "wikipedia.com",
@@ -2006,6 +2041,30 @@ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
],
)
+WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
+ "WEB_SEARCH_CONCURRENT_REQUESTS",
+ "rag.web.search.concurrent_requests",
+ int(os.getenv("WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
+)
+
+WEB_LOADER_ENGINE = PersistentConfig(
+ "WEB_LOADER_ENGINE",
+ "rag.web.loader.engine",
+ os.environ.get("WEB_LOADER_ENGINE", ""),
+)
+
+ENABLE_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
+ "ENABLE_WEB_LOADER_SSL_VERIFICATION",
+ "rag.web.loader.ssl_verification",
+ os.environ.get("ENABLE_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
+)
+
+WEB_SEARCH_TRUST_ENV = PersistentConfig(
+ "WEB_SEARCH_TRUST_ENV",
+ "rag.web.search.trust_env",
+ os.getenv("WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
+)
+
SEARXNG_QUERY_URL = PersistentConfig(
"SEARXNG_QUERY_URL",
@@ -2073,18 +2132,6 @@ SERPLY_API_KEY = PersistentConfig(
os.getenv("SERPLY_API_KEY", ""),
)
-TAVILY_API_KEY = PersistentConfig(
- "TAVILY_API_KEY",
- "rag.web.search.tavily_api_key",
- os.getenv("TAVILY_API_KEY", ""),
-)
-
-TAVILY_EXTRACT_DEPTH = PersistentConfig(
- "TAVILY_EXTRACT_DEPTH",
- "rag.web.search.tavily_extract_depth",
- os.getenv("TAVILY_EXTRACT_DEPTH", "basic"),
-)
-
JINA_API_KEY = PersistentConfig(
"JINA_API_KEY",
"rag.web.search.jina_api_key",
@@ -2141,54 +2188,55 @@ PERPLEXITY_API_KEY = PersistentConfig(
os.getenv("PERPLEXITY_API_KEY", ""),
)
-RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
- "RAG_WEB_SEARCH_RESULT_COUNT",
- "rag.web.search.result_count",
- int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")),
+SOUGOU_API_SID = PersistentConfig(
+ "SOUGOU_API_SID",
+ "rag.web.search.sougou_api_sid",
+ os.getenv("SOUGOU_API_SID", ""),
)
-RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
- "RAG_WEB_SEARCH_CONCURRENT_REQUESTS",
- "rag.web.search.concurrent_requests",
- int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
+SOUGOU_API_SK = PersistentConfig(
+ "SOUGOU_API_SK",
+ "rag.web.search.sougou_api_sk",
+ os.getenv("SOUGOU_API_SK", ""),
)
-RAG_WEB_LOADER_ENGINE = PersistentConfig(
- "RAG_WEB_LOADER_ENGINE",
- "rag.web.loader.engine",
- os.environ.get("RAG_WEB_LOADER_ENGINE", "safe_web"),
+TAVILY_API_KEY = PersistentConfig(
+ "TAVILY_API_KEY",
+ "rag.web.search.tavily_api_key",
+ os.getenv("TAVILY_API_KEY", ""),
)
-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",
+TAVILY_EXTRACT_DEPTH = PersistentConfig(
+ "TAVILY_EXTRACT_DEPTH",
+ "rag.web.search.tavily_extract_depth",
+ os.getenv("TAVILY_EXTRACT_DEPTH", "basic"),
)
-PLAYWRIGHT_WS_URI = PersistentConfig(
- "PLAYWRIGHT_WS_URI",
- "rag.web.loader.engine.playwright.ws.uri",
- os.environ.get("PLAYWRIGHT_WS_URI", None),
+PLAYWRIGHT_WS_URL = PersistentConfig(
+ "PLAYWRIGHT_WS_URL",
+ "rag.web.loader.playwright_ws_url",
+ os.environ.get("PLAYWRIGHT_WS_URL", ""),
)
PLAYWRIGHT_TIMEOUT = PersistentConfig(
"PLAYWRIGHT_TIMEOUT",
- "rag.web.loader.engine.playwright.timeout",
- int(os.environ.get("PLAYWRIGHT_TIMEOUT", "10")),
+ "rag.web.loader.playwright_timeout",
+ int(os.environ.get("PLAYWRIGHT_TIMEOUT", "10000")),
)
FIRECRAWL_API_KEY = PersistentConfig(
"FIRECRAWL_API_KEY",
- "firecrawl.api_key",
+ "rag.web.loader.firecrawl_api_key",
os.environ.get("FIRECRAWL_API_KEY", ""),
)
FIRECRAWL_API_BASE_URL = PersistentConfig(
"FIRECRAWL_API_BASE_URL",
- "firecrawl.api_url",
+ "rag.web.loader.firecrawl_api_url",
os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
)
+
####################################
# Images
####################################
@@ -2472,6 +2520,24 @@ AUDIO_STT_MODEL = PersistentConfig(
os.getenv("AUDIO_STT_MODEL", ""),
)
+AUDIO_STT_AZURE_API_KEY = PersistentConfig(
+ "AUDIO_STT_AZURE_API_KEY",
+ "audio.stt.azure.api_key",
+ os.getenv("AUDIO_STT_AZURE_API_KEY", ""),
+)
+
+AUDIO_STT_AZURE_REGION = PersistentConfig(
+ "AUDIO_STT_AZURE_REGION",
+ "audio.stt.azure.region",
+ os.getenv("AUDIO_STT_AZURE_REGION", ""),
+)
+
+AUDIO_STT_AZURE_LOCALES = PersistentConfig(
+ "AUDIO_STT_AZURE_LOCALES",
+ "audio.stt.azure.locales",
+ os.getenv("AUDIO_STT_AZURE_LOCALES", ""),
+)
+
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_TTS_OPENAI_API_BASE_URL",
"audio.tts.openai.api_base_url",
diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py
index 86d87a2c3..95c54a0d2 100644
--- a/backend/open_webui/constants.py
+++ b/backend/open_webui/constants.py
@@ -31,6 +31,7 @@ class ERROR_MESSAGES(str, Enum):
USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username."
)
+ PASSWORD_TOO_LONG = "Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long."
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py
index e3819fdc5..c9d71a4a0 100644
--- a/backend/open_webui/env.py
+++ b/backend/open_webui/env.py
@@ -326,6 +326,20 @@ REDIS_URL = os.environ.get("REDIS_URL", "")
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
+####################################
+# UVICORN WORKERS
+####################################
+
+# Number of uvicorn worker processes for handling requests
+UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1")
+try:
+ UVICORN_WORKERS = int(UVICORN_WORKERS)
+ if UVICORN_WORKERS < 1:
+ UVICORN_WORKERS = 1
+except ValueError:
+ UVICORN_WORKERS = 1
+ log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}")
+
####################################
# WEBUI_AUTH (Required for security)
####################################
@@ -408,6 +422,21 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
+
+AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get(
+ "AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10"
+)
+
+if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "":
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
+else:
+ try:
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
+ )
+ except Exception:
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
+
####################################
# OFFLINE_MODE
####################################
@@ -460,3 +489,10 @@ OTEL_TRACES_SAMPLER = os.environ.get(
PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
+
+
+####################################
+# PROGRESSIVE WEB APP OPTIONS
+####################################
+
+EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py
index c9ca059c2..1d1efc5df 100644
--- a/backend/open_webui/main.py
+++ b/backend/open_webui/main.py
@@ -148,6 +148,9 @@ from open_webui.config import (
AUDIO_STT_MODEL,
AUDIO_STT_OPENAI_API_BASE_URL,
AUDIO_STT_OPENAI_API_KEY,
+ AUDIO_STT_AZURE_API_KEY,
+ AUDIO_STT_AZURE_REGION,
+ AUDIO_STT_AZURE_LOCALES,
AUDIO_TTS_API_KEY,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
@@ -157,11 +160,11 @@ from open_webui.config import (
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
- PLAYWRIGHT_WS_URI,
+ PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
- RAG_WEB_LOADER_ENGINE,
+ WEB_LOADER_ENGINE,
WHISPER_MODEL,
DEEPGRAM_API_KEY,
WHISPER_MODEL_AUTO_UPDATE,
@@ -202,12 +205,13 @@ from open_webui.config import (
YOUTUBE_LOADER_LANGUAGE,
YOUTUBE_LOADER_PROXY_URL,
# Retrieval (Web Search)
- RAG_WEB_SEARCH_ENGINE,
+ ENABLE_WEB_SEARCH,
+ WEB_SEARCH_ENGINE,
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
- RAG_WEB_SEARCH_RESULT_COUNT,
- RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
- RAG_WEB_SEARCH_TRUST_ENV,
- RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ WEB_SEARCH_RESULT_COUNT,
+ WEB_SEARCH_CONCURRENT_REQUESTS,
+ WEB_SEARCH_TRUST_ENV,
+ WEB_SEARCH_DOMAIN_FILTER_LIST,
JINA_API_KEY,
SEARCHAPI_API_KEY,
SEARCHAPI_ENGINE,
@@ -225,6 +229,8 @@ from open_webui.config import (
BRAVE_SEARCH_API_KEY,
EXA_API_KEY,
PERPLEXITY_API_KEY,
+ SOUGOU_API_SID,
+ SOUGOU_API_SK,
KAGI_SEARCH_API_KEY,
MOJEEK_SEARCH_API_KEY,
BOCHA_SEARCH_API_KEY,
@@ -235,8 +241,7 @@ from open_webui.config import (
ONEDRIVE_CLIENT_ID,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
- ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
- ENABLE_RAG_WEB_SEARCH,
+ ENABLE_WEB_LOADER_SSL_VERIFICATION,
ENABLE_GOOGLE_DRIVE_INTEGRATION,
ENABLE_ONEDRIVE_INTEGRATION,
UPLOAD_DIR,
@@ -340,6 +345,7 @@ from open_webui.env import (
RESET_CONFIG_ON_START,
OFFLINE_MODE,
ENABLE_OTEL,
+ EXTERNAL_PWA_MANIFEST_URL,
)
@@ -366,7 +372,11 @@ from open_webui.utils.auth import (
from open_webui.utils.oauth import OAuthManager
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 (
+ list_task_ids_by_chat_id,
+ stop_task,
+ list_tasks,
+) # Import from tasks.py
from open_webui.utils.redis import get_sentinels_from_env
@@ -426,6 +436,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
+ title="Open WebUI",
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
@@ -564,6 +575,7 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
+app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
app.state.USER_COUNT = None
app.state.TOOLS = {}
@@ -586,9 +598,7 @@ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
-app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
- ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
-)
+app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
@@ -621,12 +631,16 @@ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
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.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
+app.state.config.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH
+app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE
+app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST
+app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT
+app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS
+app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE
+app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV
app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
-app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
@@ -651,12 +665,11 @@ 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.EXA_API_KEY = EXA_API_KEY
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
+app.state.config.SOUGOU_API_SID = SOUGOU_API_SID
+app.state.config.SOUGOU_API_SK = SOUGOU_API_SK
-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_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.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT
app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
@@ -778,6 +791,10 @@ app.state.config.STT_MODEL = AUDIO_STT_MODEL
app.state.config.WHISPER_MODEL = WHISPER_MODEL
app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
+app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
+app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION
+app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
+
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
@@ -1053,6 +1070,7 @@ async def chat_completion(
model_item = form_data.pop("model_item", {})
tasks = form_data.pop("background_tasks", None)
+ metadata = {}
try:
if not model_item.get("direct", False):
model_id = form_data.get("model", None)
@@ -1108,13 +1126,15 @@ async def chat_completion(
except Exception as e:
log.debug(f"Error processing chat payload: {e}")
- Chats.upsert_message_to_chat_by_id_and_message_id(
- metadata["chat_id"],
- metadata["message_id"],
- {
- "error": {"content": str(e)},
- },
- )
+ if metadata.get("chat_id") and metadata.get("message_id"):
+ # Update the chat message with the error
+ Chats.upsert_message_to_chat_by_id_and_message_id(
+ metadata["chat_id"],
+ metadata["message_id"],
+ {
+ "error": {"content": str(e)},
+ },
+ )
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -1180,7 +1200,7 @@ async def chat_action(
@app.post("/api/tasks/stop/{task_id}")
async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
try:
- result = await stop_task(task_id) # Use the function from tasks.py
+ result = await stop_task(task_id)
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
@@ -1188,7 +1208,19 @@ async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
@app.get("/api/tasks")
async def list_tasks_endpoint(user=Depends(get_verified_user)):
- return {"tasks": list_tasks()} # Use the function from tasks.py
+ return {"tasks": list_tasks()}
+
+
+@app.get("/api/tasks/chat/{chat_id}")
+async def list_tasks_by_chat_id_endpoint(chat_id: str, user=Depends(get_verified_user)):
+ chat = Chats.get_chat_by_id(chat_id)
+ if chat is None or chat.user_id != user.id:
+ return {"task_ids": []}
+
+ task_ids = list_task_ids_by_chat_id(chat_id)
+
+ print(f"Task IDs for chat {chat_id}: {task_ids}")
+ return {"task_ids": task_ids}
##################################
@@ -1244,7 +1276,7 @@ async def get_app_config(request: Request):
{
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
"enable_channels": app.state.config.ENABLE_CHANNELS,
- "enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
+ "enable_web_search": app.state.config.ENABLE_WEB_SEARCH,
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
@@ -1390,29 +1422,32 @@ async def oauth_callback(provider: str, request: Request, response: Response):
@app.get("/manifest.json")
async def get_manifest_json():
- return {
- "name": app.state.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.",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#343541",
- "orientation": "natural",
- "icons": [
- {
- "src": "/static/logo.png",
- "type": "image/png",
- "sizes": "500x500",
- "purpose": "any",
- },
- {
- "src": "/static/logo.png",
- "type": "image/png",
- "sizes": "500x500",
- "purpose": "maskable",
- },
- ],
- }
+ if app.state.EXTERNAL_PWA_MANIFEST_URL:
+ return requests.get(app.state.EXTERNAL_PWA_MANIFEST_URL).json()
+ else:
+ return {
+ "name": app.state.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.",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#343541",
+ "orientation": "natural",
+ "icons": [
+ {
+ "src": "/static/logo.png",
+ "type": "image/png",
+ "sizes": "500x500",
+ "purpose": "any",
+ },
+ {
+ "src": "/static/logo.png",
+ "type": "image/png",
+ "sizes": "500x500",
+ "purpose": "maskable",
+ },
+ ],
+ }
@app.get("/opensearch.xml")
diff --git a/backend/open_webui/models/memories.py b/backend/open_webui/models/memories.py
index c8dae9726..8b10a77cf 100644
--- a/backend/open_webui/models/memories.py
+++ b/backend/open_webui/models/memories.py
@@ -63,14 +63,15 @@ class MemoriesTable:
else:
return None
- def update_memory_by_id(
+ def update_memory_by_id_and_user_id(
self,
id: str,
+ user_id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
try:
- db.query(Memory).filter_by(id=id).update(
+ db.query(Memory).filter_by(id=id, user_id=user_id).update(
{"content": content, "updated_at": int(time.time())}
)
db.commit()
diff --git a/backend/open_webui/retrieval/loaders/youtube.py b/backend/open_webui/retrieval/loaders/youtube.py
index 8eb48488b..f59dd7df5 100644
--- a/backend/open_webui/retrieval/loaders/youtube.py
+++ b/backend/open_webui/retrieval/loaders/youtube.py
@@ -110,7 +110,7 @@ class YoutubeLoader:
transcript = " ".join(
map(
- lambda transcript_piece: transcript_piece["text"].strip(" "),
+ lambda transcript_piece: transcript_piece.text.strip(" "),
transcript_pieces,
)
)
diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py
index 12d48f869..2b23cad17 100644
--- a/backend/open_webui/retrieval/utils.py
+++ b/backend/open_webui/retrieval/utils.py
@@ -77,6 +77,7 @@ def query_doc(
collection_name: str, query_embedding: list[float], k: int, user: UserModel = None
):
try:
+ log.debug(f"query_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.search(
collection_name=collection_name,
vectors=[query_embedding],
@@ -94,6 +95,7 @@ def query_doc(
def get_doc(collection_name: str, user: UserModel = None):
try:
+ log.debug(f"get_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
if result:
@@ -116,6 +118,7 @@ def query_doc_with_hybrid_search(
r: float,
) -> dict:
try:
+ log.debug(f"query_doc_with_hybrid_search:doc {collection_name}")
bm25_retriever = BM25Retriever.from_texts(
texts=collection_result.documents[0],
metadatas=collection_result.metadatas[0],
@@ -168,6 +171,7 @@ def query_doc_with_hybrid_search(
)
return result
except Exception as e:
+ log.exception(f"Error querying doc {collection_name} with hybrid search: {e}")
raise e
@@ -257,6 +261,7 @@ def query_collection(
) -> dict:
results = []
for query in queries:
+ log.debug(f"query_collection:query {query}")
query_embedding = embedding_function(query, prefix=RAG_EMBEDDING_QUERY_PREFIX)
for collection_name in collection_names:
if collection_name:
@@ -292,6 +297,9 @@ def query_collection_with_hybrid_search(
collection_results = {}
for collection_name in collection_names:
try:
+ log.debug(
+ f"query_collection_with_hybrid_search:VECTOR_DB_CLIENT.get:collection {collection_name}"
+ )
collection_results[collection_name] = VECTOR_DB_CLIENT.get(
collection_name=collection_name
)
@@ -613,6 +621,9 @@ def generate_openai_batch_embeddings(
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
+ log.debug(
+ f"generate_openai_batch_embeddings:model {model} batch size: {len(texts)}"
+ )
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
@@ -655,6 +666,9 @@ def generate_ollama_batch_embeddings(
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
+ log.debug(
+ f"generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}"
+ )
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
diff --git a/backend/open_webui/retrieval/web/duckduckgo.py b/backend/open_webui/retrieval/web/duckduckgo.py
index d95086671..bf8ae6880 100644
--- a/backend/open_webui/retrieval/web/duckduckgo.py
+++ b/backend/open_webui/retrieval/web/duckduckgo.py
@@ -3,6 +3,7 @@ from typing import Optional
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
+from duckduckgo_search.exceptions import RatelimitException
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -22,16 +23,15 @@ def search_duckduckgo(
list[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
+ search_results = []
with DDGS() as ddgs:
# Use the ddgs.text() method to perform the search
- ddgs_gen = ddgs.text(
- query, safesearch="moderate", max_results=count, backend="api"
- )
- # Check if there are search results
- if ddgs_gen:
- # Convert the search results into a list
- search_results = [r for r in ddgs_gen]
-
+ try:
+ search_results = ddgs.text(
+ query, safesearch="moderate", max_results=count, backend="lite"
+ )
+ except RatelimitException as e:
+ log.error(f"RatelimitException: {e}")
if filter_list:
search_results = get_filtered_results(search_results, filter_list)
diff --git a/backend/open_webui/retrieval/web/sougou.py b/backend/open_webui/retrieval/web/sougou.py
new file mode 100644
index 000000000..af7957c4f
--- /dev/null
+++ b/backend/open_webui/retrieval/web/sougou.py
@@ -0,0 +1,60 @@
+import logging
+import json
+from typing import Optional, List
+
+
+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_sougou(
+ sougou_api_sid: str,
+ sougou_api_sk: str,
+ query: str,
+ count: int,
+ filter_list: Optional[List[str]] = None,
+) -> List[SearchResult]:
+ from tencentcloud.common.common_client import CommonClient
+ from tencentcloud.common import credential
+ from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
+ TencentCloudSDKException,
+ )
+ from tencentcloud.common.profile.client_profile import ClientProfile
+ from tencentcloud.common.profile.http_profile import HttpProfile
+
+ try:
+ cred = credential.Credential(sougou_api_sid, sougou_api_sk)
+ http_profile = HttpProfile()
+ http_profile.endpoint = "tms.tencentcloudapi.com"
+ client_profile = ClientProfile()
+ client_profile.http_profile = http_profile
+ params = json.dumps({"Query": query, "Cnt": 20})
+ common_client = CommonClient(
+ "tms", "2020-12-29", cred, "", profile=client_profile
+ )
+ results = [
+ json.loads(page)
+ for page in common_client.call_json("SearchPro", json.loads(params))[
+ "Response"
+ ]["Pages"]
+ ]
+ sorted_results = sorted(
+ results, key=lambda x: x.get("scour", 0.0), reverse=True
+ )
+ if filter_list:
+ sorted_results = get_filtered_results(sorted_results, filter_list)
+
+ return [
+ SearchResult(
+ link=result.get("url"),
+ title=result.get("title"),
+ snippet=result.get("passage"),
+ )
+ for result in sorted_results[:count]
+ ]
+ except TencentCloudSDKException as err:
+ log.error(f"Error in Sougou search: {err}")
+ return []
diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py
index 942cb8483..718cfe52f 100644
--- a/backend/open_webui/retrieval/web/utils.py
+++ b/backend/open_webui/retrieval/web/utils.py
@@ -28,9 +28,9 @@ from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
- PLAYWRIGHT_WS_URI,
+ PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
- RAG_WEB_LOADER_ENGINE,
+ WEB_LOADER_ENGINE,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
TAVILY_API_KEY,
@@ -584,13 +584,6 @@ class SafeWebBaseLoader(WebBaseLoader):
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
-RAG_WEB_LOADER_ENGINES["tavily"] = SafeTavilyLoader
-
-
def get_web_loader(
urls: Union[str, Sequence[str]],
verify_ssl: bool = True,
@@ -608,27 +601,36 @@ def get_web_loader(
"trust_env": trust_env,
}
- if RAG_WEB_LOADER_ENGINE.value == "playwright":
+ if WEB_LOADER_ENGINE.value == "" or WEB_LOADER_ENGINE.value == "safe_web":
+ WebLoaderClass = SafeWebBaseLoader
+ if WEB_LOADER_ENGINE.value == "playwright":
+ WebLoaderClass = SafePlaywrightURLLoader
web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value * 1000
- if PLAYWRIGHT_WS_URI.value:
- web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URI.value
+ if PLAYWRIGHT_WS_URL.value:
+ web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value
- if RAG_WEB_LOADER_ENGINE.value == "firecrawl":
+ if WEB_LOADER_ENGINE.value == "firecrawl":
+ WebLoaderClass = SafeFireCrawlLoader
web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
- if RAG_WEB_LOADER_ENGINE.value == "tavily":
+ if WEB_LOADER_ENGINE.value == "tavily":
+ WebLoaderClass = SafeTavilyLoader
web_loader_args["api_key"] = TAVILY_API_KEY.value
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.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)
+ if WebLoaderClass:
+ web_loader = WebLoaderClass(**web_loader_args)
- log.debug(
- "Using RAG_WEB_LOADER_ENGINE %s for %s URLs",
- web_loader.__class__.__name__,
- len(safe_urls),
- )
+ log.debug(
+ "Using WEB_LOADER_ENGINE %s for %s URLs",
+ web_loader.__class__.__name__,
+ len(safe_urls),
+ )
- return web_loader
+ return web_loader
+ else:
+ raise ValueError(
+ f"Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. "
+ "Please set it to 'safe_web', 'playwright', 'firecrawl', or 'tavily'."
+ )
diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py
index ea1372623..a5447e1fc 100644
--- a/backend/open_webui/routers/audio.py
+++ b/backend/open_webui/routers/audio.py
@@ -50,6 +50,8 @@ router = APIRouter()
# Constants
MAX_FILE_SIZE_MB = 25
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
+AZURE_MAX_FILE_SIZE_MB = 200
+AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
@@ -68,8 +70,8 @@ from pydub import AudioSegment
from pydub.utils import mediainfo
-def is_mp4_audio(file_path):
- """Check if the given file is an MP4 audio file."""
+def get_audio_format(file_path):
+ """Check if the given file needs to be converted to a different format."""
if not os.path.isfile(file_path):
log.error(f"File not found: {file_path}")
return False
@@ -80,13 +82,17 @@ def is_mp4_audio(file_path):
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
- return True
- return False
+ return "mp4"
+ elif info.get("format_name") == "ogg":
+ return "ogg"
+ elif info.get("format_name") == "matroska,webm":
+ return "webm"
+ return None
-def convert_mp4_to_wav(file_path, output_path):
- """Convert MP4 audio file to WAV format."""
- audio = AudioSegment.from_file(file_path, format="mp4")
+def convert_audio_to_wav(file_path, output_path, conversion_type):
+ """Convert MP4/OGG audio file to WAV format."""
+ audio = AudioSegment.from_file(file_path, format=conversion_type)
audio.export(output_path, format="wav")
log.info(f"Converted {file_path} to {output_path}")
@@ -141,6 +147,9 @@ class STTConfigForm(BaseModel):
MODEL: str
WHISPER_MODEL: str
DEEPGRAM_API_KEY: str
+ AZURE_API_KEY: str
+ AZURE_REGION: str
+ AZURE_LOCALES: str
class AudioConfigUpdateForm(BaseModel):
@@ -169,6 +178,9 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"MODEL": request.app.state.config.STT_MODEL,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
"DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
+ "AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
+ "AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
+ "AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
},
}
@@ -195,6 +207,9 @@ async def update_audio_config(
request.app.state.config.STT_MODEL = form_data.stt.MODEL
request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY
+ request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY
+ request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION
+ request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
@@ -220,6 +235,9 @@ async def update_audio_config(
"MODEL": request.app.state.config.STT_MODEL,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
"DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
+ "AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
+ "AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
+ "AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
},
}
@@ -312,7 +330,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
- status_code=getattr(r, "status", 500),
+ status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -366,7 +384,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
- status_code=getattr(r, "status", 500),
+ status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -422,7 +440,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
- status_code=getattr(r, "status", 500),
+ status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -496,10 +514,15 @@ def transcribe(request: Request, file_path):
log.debug(data)
return data
elif request.app.state.config.STT_ENGINE == "openai":
- if is_mp4_audio(file_path):
- os.rename(file_path, file_path.replace(".wav", ".mp4"))
- # Convert MP4 audio file to WAV format
- convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
+ audio_format = get_audio_format(file_path)
+ if audio_format:
+ os.rename(file_path, file_path.replace(".wav", f".{audio_format}"))
+ # Convert unsupported audio file to WAV format
+ convert_audio_to_wav(
+ file_path.replace(".wav", f".{audio_format}"),
+ file_path,
+ audio_format,
+ )
r = None
try:
@@ -598,6 +621,119 @@ def transcribe(request: Request, file_path):
detail = f"External: {e}"
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
+ elif request.app.state.config.STT_ENGINE == "azure":
+ # Check file exists and size
+ if not os.path.exists(file_path):
+ raise HTTPException(status_code=400, detail="Audio file not found")
+
+ # Check file size (Azure has a larger limit of 200MB)
+ file_size = os.path.getsize(file_path)
+ if file_size > AZURE_MAX_FILE_SIZE:
+ raise HTTPException(
+ status_code=400,
+ detail=f"File size exceeds Azure's limit of {AZURE_MAX_FILE_SIZE_MB}MB",
+ )
+
+ api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
+ region = request.app.state.config.AUDIO_STT_AZURE_REGION
+ locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES
+
+ # IF NO LOCALES, USE DEFAULTS
+ if len(locales) < 2:
+ locales = [
+ "en-US",
+ "es-ES",
+ "es-MX",
+ "fr-FR",
+ "hi-IN",
+ "it-IT",
+ "de-DE",
+ "en-GB",
+ "en-IN",
+ "ja-JP",
+ "ko-KR",
+ "pt-BR",
+ "zh-CN",
+ ]
+ locales = ",".join(locales)
+
+ if not api_key or not region:
+ raise HTTPException(
+ status_code=400,
+ detail="Azure API key and region are required for Azure STT",
+ )
+
+ r = None
+ try:
+ # Prepare the request
+ data = {
+ "definition": json.dumps(
+ {
+ "locales": locales.split(","),
+ "diarization": {"maxSpeakers": 3, "enabled": True},
+ }
+ if locales
+ else {}
+ )
+ }
+ url = f"https://{region}.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
+
+ # Use context manager to ensure file is properly closed
+ with open(file_path, "rb") as audio_file:
+ r = requests.post(
+ url=url,
+ files={"audio": audio_file},
+ data=data,
+ headers={
+ "Ocp-Apim-Subscription-Key": api_key,
+ },
+ )
+
+ r.raise_for_status()
+ response = r.json()
+
+ # Extract transcript from response
+ if not response.get("combinedPhrases"):
+ raise ValueError("No transcription found in response")
+
+ # Get the full transcript from combinedPhrases
+ transcript = response["combinedPhrases"][0].get("text", "").strip()
+ if not transcript:
+ raise ValueError("Empty transcript in response")
+
+ data = {"text": transcript}
+
+ # Save transcript to json file (consistent with other providers)
+ transcript_file = f"{file_dir}/{id}.json"
+ with open(transcript_file, "w") as f:
+ json.dump(data, f)
+
+ log.debug(data)
+ return data
+
+ except (KeyError, IndexError, ValueError) as e:
+ log.exception("Error parsing Azure response")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse Azure response: {str(e)}",
+ )
+ except requests.exceptions.RequestException as e:
+ log.exception(e)
+ detail = None
+
+ try:
+ if r is not None and r.status_code != 200:
+ res = r.json()
+ if "error" in res:
+ detail = f"External: {res['error'].get('message', '')}"
+ except Exception:
+ detail = f"External: {e}"
+
+ raise HTTPException(
+ status_code=getattr(r, "status_code", 500) if r else 500,
+ detail=detail if detail else "Open WebUI: Server Connection Error",
+ )
+
def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE:
diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py
index 67c2e9f2a..6574ef0b1 100644
--- a/backend/open_webui/routers/auths.py
+++ b/backend/open_webui/routers/auths.py
@@ -230,11 +230,13 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
entry = connection_app.entries[0]
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
- email = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
- if not email or email == "" or email == "[]":
+ email = entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"]
+ if not email:
raise HTTPException(400, "User does not have a valid email address.")
- else:
+ elif isinstance(email, str):
email = email.lower()
+ elif isinstance(email, list):
+ email = email[0].lower()
cn = str(entry["cn"])
user_dn = entry.entry_dn
@@ -454,6 +456,13 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
+ # The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
+ if len(form_data.password.encode("utf-8")) > 72:
+ raise HTTPException(
+ status.HTTP_400_BAD_REQUEST,
+ detail=ERROR_MESSAGES.PASSWORD_TOO_LONG,
+ )
+
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py
index 74bb96c94..5fd44ab9f 100644
--- a/backend/open_webui/routers/chats.py
+++ b/backend/open_webui/routers/chats.py
@@ -579,7 +579,12 @@ async def clone_chat_by_id(
@router.post("/{id}/clone/shared", response_model=Optional[ChatResponse])
async def clone_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
- chat = Chats.get_chat_by_share_id(id)
+
+ if user.role == "admin":
+ chat = Chats.get_chat_by_id(id)
+ else:
+ chat = Chats.get_chat_by_share_id(id)
+
if chat:
updated_chat = {
**chat.chat,
diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py
index c30366545..8a2888d86 100644
--- a/backend/open_webui/routers/files.py
+++ b/backend/open_webui/routers/files.py
@@ -1,6 +1,7 @@
import logging
import os
import uuid
+from fnmatch import fnmatch
from pathlib import Path
from typing import Optional
from urllib.parse import quote
@@ -177,6 +178,47 @@ async def list_files(user=Depends(get_verified_user), content: bool = Query(True
return files
+############################
+# Search Files
+############################
+
+
+@router.get("/search", response_model=list[FileModelResponse])
+async def search_files(
+ filename: str = Query(
+ ...,
+ description="Filename pattern to search for. Supports wildcards such as '*.txt'",
+ ),
+ content: bool = Query(True),
+ user=Depends(get_verified_user),
+):
+ """
+ Search for files by filename with support for wildcard patterns.
+ """
+ # Get files according to user role
+ if user.role == "admin":
+ files = Files.get_files()
+ else:
+ files = Files.get_files_by_user_id(user.id)
+
+ # Get matching files
+ matching_files = [
+ file for file in files if fnmatch(file.filename.lower(), filename.lower())
+ ]
+
+ if not matching_files:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="No files found matching the pattern.",
+ )
+
+ if not content:
+ for file in matching_files:
+ del file.data["content"]
+
+ return matching_files
+
+
############################
# Delete All Files
############################
diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py
index bc1e2429e..15547afa7 100644
--- a/backend/open_webui/routers/knowledge.py
+++ b/backend/open_webui/routers/knowledge.py
@@ -161,6 +161,74 @@ async def create_new_knowledge(
)
+############################
+# ReindexKnowledgeFiles
+############################
+
+
+@router.post("/reindex", response_model=bool)
+async def reindex_knowledge_files(request: Request, user=Depends(get_verified_user)):
+ if user.role != "admin":
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=ERROR_MESSAGES.UNAUTHORIZED,
+ )
+
+ knowledge_bases = Knowledges.get_knowledge_bases()
+
+ log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
+
+ for knowledge_base in knowledge_bases:
+ try:
+ files = Files.get_files_by_ids(knowledge_base.data.get("file_ids", []))
+
+ try:
+ if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
+ VECTOR_DB_CLIENT.delete_collection(
+ collection_name=knowledge_base.id
+ )
+ except Exception as e:
+ log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error deleting vector DB collection",
+ )
+
+ failed_files = []
+ for file in files:
+ try:
+ process_file(
+ request,
+ ProcessFileForm(
+ file_id=file.id, collection_name=knowledge_base.id
+ ),
+ user=user,
+ )
+ except Exception as e:
+ log.error(
+ f"Error processing file {file.filename} (ID: {file.id}): {str(e)}"
+ )
+ failed_files.append({"file_id": file.id, "error": str(e)})
+ continue
+
+ except Exception as e:
+ log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error processing knowledge base",
+ )
+
+ if failed_files:
+ log.warning(
+ f"Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}"
+ )
+ for failed in failed_files:
+ log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}")
+
+ log.info("Reindexing completed successfully")
+ return True
+
+
############################
# GetKnowledgeById
############################
diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py
index e660ef852..6d54c9c17 100644
--- a/backend/open_webui/routers/memories.py
+++ b/backend/open_webui/routers/memories.py
@@ -153,7 +153,9 @@ async def update_memory_by_id(
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
- memory = Memories.update_memory_by_id(memory_id, form_data.content)
+ memory = Memories.update_memory_by_id_and_user_id(
+ memory_id, user.id, form_data.content
+ )
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py
index f31abd9ff..d46b8b393 100644
--- a/backend/open_webui/routers/retrieval.py
+++ b/backend/open_webui/routers/retrieval.py
@@ -60,6 +60,7 @@ from open_webui.retrieval.web.tavily import search_tavily
from open_webui.retrieval.web.bing import search_bing
from open_webui.retrieval.web.exa import search_exa
from open_webui.retrieval.web.perplexity import search_perplexity
+from open_webui.retrieval.web.sougou import search_sougou
from open_webui.retrieval.utils import (
get_embedding_function,
@@ -351,427 +352,429 @@ async def update_reranking_config(
async def get_rag_config(request: Request, user=Depends(get_admin_user)):
return {
"status": True,
- "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
- "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
+ # RAG settings
+ "TEMPLATE": request.app.state.config.RAG_TEMPLATE,
+ "TOP_K": request.app.state.config.TOP_K,
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
- "enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
- "enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
- "content_extraction": {
- "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
- "tika_server_url": request.app.state.config.TIKA_SERVER_URL,
- "docling_server_url": request.app.state.config.DOCLING_SERVER_URL,
- "document_intelligence_config": {
- "endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
- "key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
- },
- "mistral_ocr_config": {
- "api_key": request.app.state.config.MISTRAL_OCR_API_KEY,
- },
- },
- "chunk": {
- "text_splitter": request.app.state.config.TEXT_SPLITTER,
- "chunk_size": request.app.state.config.CHUNK_SIZE,
- "chunk_overlap": request.app.state.config.CHUNK_OVERLAP,
- },
- "file": {
- "max_size": request.app.state.config.FILE_MAX_SIZE,
- "max_count": request.app.state.config.FILE_MAX_COUNT,
- },
- "youtube": {
- "language": request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
- "translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
- "proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
- },
+ "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
+ # Hybrid search settings
+ "ENABLE_RAG_HYBRID_SEARCH": request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
+ "TOP_K_RERANKER": request.app.state.config.TOP_K_RERANKER,
+ "RELEVANCE_THRESHOLD": request.app.state.config.RELEVANCE_THRESHOLD,
+ # Content extraction settings
+ "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
+ "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES,
+ "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
+ "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
+ "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
+ "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
+ "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
+ # Chunking settings
+ "TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER,
+ "CHUNK_SIZE": request.app.state.config.CHUNK_SIZE,
+ "CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP,
+ # File upload settings
+ "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
+ "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
+ # Integration settings
+ "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
+ "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
+ # Web search settings
"web": {
- "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+ "ENABLE_WEB_SEARCH": request.app.state.config.ENABLE_WEB_SEARCH,
+ "WEB_SEARCH_ENGINE": request.app.state.config.WEB_SEARCH_ENGINE,
+ "WEB_SEARCH_TRUST_ENV": request.app.state.config.WEB_SEARCH_TRUST_ENV,
+ "WEB_SEARCH_RESULT_COUNT": request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ "WEB_SEARCH_CONCURRENT_REQUESTS": request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS,
+ "WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
- "search": {
- "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
- "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
- "onedrive": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
- "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
- "searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
- "google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
- "google_pse_engine_id": request.app.state.config.GOOGLE_PSE_ENGINE_ID,
- "brave_search_api_key": request.app.state.config.BRAVE_SEARCH_API_KEY,
- "kagi_search_api_key": request.app.state.config.KAGI_SEARCH_API_KEY,
- "mojeek_search_api_key": request.app.state.config.MOJEEK_SEARCH_API_KEY,
- "bocha_search_api_key": request.app.state.config.BOCHA_SEARCH_API_KEY,
- "serpstack_api_key": request.app.state.config.SERPSTACK_API_KEY,
- "serpstack_https": request.app.state.config.SERPSTACK_HTTPS,
- "serper_api_key": request.app.state.config.SERPER_API_KEY,
- "serply_api_key": request.app.state.config.SERPLY_API_KEY,
- "tavily_api_key": request.app.state.config.TAVILY_API_KEY,
- "searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
- "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,
- "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,
- "exa_api_key": request.app.state.config.EXA_API_KEY,
- "perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
- "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- "trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
- "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
- "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
- },
+ "SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
+ "GOOGLE_PSE_API_KEY": request.app.state.config.GOOGLE_PSE_API_KEY,
+ "GOOGLE_PSE_ENGINE_ID": request.app.state.config.GOOGLE_PSE_ENGINE_ID,
+ "BRAVE_SEARCH_API_KEY": request.app.state.config.BRAVE_SEARCH_API_KEY,
+ "KAGI_SEARCH_API_KEY": request.app.state.config.KAGI_SEARCH_API_KEY,
+ "MOJEEK_SEARCH_API_KEY": request.app.state.config.MOJEEK_SEARCH_API_KEY,
+ "BOCHA_SEARCH_API_KEY": request.app.state.config.BOCHA_SEARCH_API_KEY,
+ "SERPSTACK_API_KEY": request.app.state.config.SERPSTACK_API_KEY,
+ "SERPSTACK_HTTPS": request.app.state.config.SERPSTACK_HTTPS,
+ "SERPER_API_KEY": request.app.state.config.SERPER_API_KEY,
+ "SERPLY_API_KEY": request.app.state.config.SERPLY_API_KEY,
+ "TAVILY_API_KEY": request.app.state.config.TAVILY_API_KEY,
+ "SEARCHAPI_API_KEY": request.app.state.config.SEARCHAPI_API_KEY,
+ "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,
+ "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,
+ "EXA_API_KEY": request.app.state.config.EXA_API_KEY,
+ "PERPLEXITY_API_KEY": request.app.state.config.PERPLEXITY_API_KEY,
+ "SOUGOU_API_SID": request.app.state.config.SOUGOU_API_SID,
+ "SOUGOU_API_SK": request.app.state.config.SOUGOU_API_SK,
+ "WEB_LOADER_ENGINE": request.app.state.config.WEB_LOADER_ENGINE,
+ "ENABLE_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
+ "PLAYWRIGHT_WS_URL": request.app.state.config.PLAYWRIGHT_WS_URL,
+ "PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT,
+ "FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY,
+ "FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL,
+ "TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH,
+ "YOUTUBE_LOADER_LANGUAGE": request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
+ "YOUTUBE_LOADER_PROXY_URL": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
+ "YOUTUBE_LOADER_TRANSLATION": request.app.state.YOUTUBE_LOADER_TRANSLATION,
},
}
-class FileConfig(BaseModel):
- max_size: Optional[int] = None
- max_count: Optional[int] = None
-
-
-class DocumentIntelligenceConfigForm(BaseModel):
- endpoint: str
- key: str
-
-
-class MistralOCRConfigForm(BaseModel):
- api_key: str
-
-
-class ContentExtractionConfig(BaseModel):
- engine: str = ""
- tika_server_url: Optional[str] = None
- docling_server_url: Optional[str] = None
- document_intelligence_config: Optional[DocumentIntelligenceConfigForm] = None
- mistral_ocr_config: Optional[MistralOCRConfigForm] = None
-
-
-class ChunkParamUpdateForm(BaseModel):
- text_splitter: Optional[str] = None
- chunk_size: int
- chunk_overlap: int
-
-
-class YoutubeLoaderConfig(BaseModel):
- language: list[str]
- translation: Optional[str] = None
- proxy_url: str = ""
-
-
-class WebSearchConfig(BaseModel):
- enabled: bool
- engine: Optional[str] = None
- searxng_query_url: Optional[str] = None
- google_pse_api_key: Optional[str] = None
- google_pse_engine_id: Optional[str] = None
- brave_search_api_key: Optional[str] = None
- kagi_search_api_key: Optional[str] = None
- mojeek_search_api_key: Optional[str] = None
- bocha_search_api_key: Optional[str] = None
- serpstack_api_key: Optional[str] = None
- serpstack_https: Optional[bool] = None
- serper_api_key: Optional[str] = None
- serply_api_key: Optional[str] = None
- tavily_api_key: Optional[str] = None
- searchapi_api_key: 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
- bing_search_v7_endpoint: Optional[str] = None
- bing_search_v7_subscription_key: Optional[str] = None
- exa_api_key: Optional[str] = None
- perplexity_api_key: Optional[str] = None
- result_count: Optional[int] = None
- concurrent_requests: Optional[int] = None
- trust_env: Optional[bool] = None
- domain_filter_list: Optional[List[str]] = []
-
-
class WebConfig(BaseModel):
- search: WebSearchConfig
- ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
+ ENABLE_WEB_SEARCH: Optional[bool] = None
+ WEB_SEARCH_ENGINE: Optional[str] = None
+ WEB_SEARCH_TRUST_ENV: Optional[bool] = None
+ WEB_SEARCH_RESULT_COUNT: Optional[int] = None
+ WEB_SEARCH_CONCURRENT_REQUESTS: Optional[int] = None
+ WEB_SEARCH_DOMAIN_FILTER_LIST: Optional[List[str]] = []
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
+ SEARXNG_QUERY_URL: Optional[str] = None
+ GOOGLE_PSE_API_KEY: Optional[str] = None
+ GOOGLE_PSE_ENGINE_ID: Optional[str] = None
+ BRAVE_SEARCH_API_KEY: Optional[str] = None
+ KAGI_SEARCH_API_KEY: Optional[str] = None
+ MOJEEK_SEARCH_API_KEY: Optional[str] = None
+ BOCHA_SEARCH_API_KEY: Optional[str] = None
+ SERPSTACK_API_KEY: Optional[str] = None
+ SERPSTACK_HTTPS: Optional[bool] = None
+ SERPER_API_KEY: Optional[str] = None
+ SERPLY_API_KEY: Optional[str] = None
+ TAVILY_API_KEY: Optional[str] = None
+ SEARCHAPI_API_KEY: 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
+ BING_SEARCH_V7_ENDPOINT: Optional[str] = None
+ BING_SEARCH_V7_SUBSCRIPTION_KEY: Optional[str] = None
+ EXA_API_KEY: Optional[str] = None
+ PERPLEXITY_API_KEY: Optional[str] = None
+ SOUGOU_API_SID: Optional[str] = None
+ SOUGOU_API_SK: Optional[str] = None
+ WEB_LOADER_ENGINE: Optional[str] = None
+ ENABLE_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
+ PLAYWRIGHT_WS_URL: Optional[str] = None
+ PLAYWRIGHT_TIMEOUT: Optional[int] = None
+ FIRECRAWL_API_KEY: Optional[str] = None
+ FIRECRAWL_API_BASE_URL: Optional[str] = None
+ TAVILY_EXTRACT_DEPTH: Optional[str] = None
+ YOUTUBE_LOADER_LANGUAGE: Optional[List[str]] = None
+ YOUTUBE_LOADER_PROXY_URL: Optional[str] = None
+ YOUTUBE_LOADER_TRANSLATION: Optional[str] = None
-class ConfigUpdateForm(BaseModel):
- RAG_FULL_CONTEXT: Optional[bool] = None
+class ConfigForm(BaseModel):
+ # RAG settings
+ TEMPLATE: Optional[str] = None
+ TOP_K: Optional[int] = None
BYPASS_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
- pdf_extract_images: Optional[bool] = None
- enable_google_drive_integration: Optional[bool] = None
- enable_onedrive_integration: Optional[bool] = None
- file: Optional[FileConfig] = None
- content_extraction: Optional[ContentExtractionConfig] = None
- chunk: Optional[ChunkParamUpdateForm] = None
- youtube: Optional[YoutubeLoaderConfig] = None
+ RAG_FULL_CONTEXT: Optional[bool] = None
+
+ # Hybrid search settings
+ ENABLE_RAG_HYBRID_SEARCH: Optional[bool] = None
+ TOP_K_RERANKER: Optional[int] = None
+ RELEVANCE_THRESHOLD: Optional[float] = None
+
+ # Content extraction settings
+ CONTENT_EXTRACTION_ENGINE: Optional[str] = None
+ PDF_EXTRACT_IMAGES: Optional[bool] = None
+ TIKA_SERVER_URL: Optional[str] = None
+ DOCLING_SERVER_URL: Optional[str] = None
+ DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
+ DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
+ MISTRAL_OCR_API_KEY: Optional[str] = None
+
+ # Chunking settings
+ TEXT_SPLITTER: Optional[str] = None
+ CHUNK_SIZE: Optional[int] = None
+ CHUNK_OVERLAP: Optional[int] = None
+
+ # File upload settings
+ FILE_MAX_SIZE: Optional[int] = None
+ FILE_MAX_COUNT: Optional[int] = None
+
+ # Integration settings
+ ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None
+ ENABLE_ONEDRIVE_INTEGRATION: Optional[bool] = None
+
+ # Web search settings
web: Optional[WebConfig] = None
@router.post("/config/update")
async def update_rag_config(
- request: Request, form_data: ConfigUpdateForm, user=Depends(get_admin_user)
+ request: Request, form_data: ConfigForm, user=Depends(get_admin_user)
):
- request.app.state.config.PDF_EXTRACT_IMAGES = (
- form_data.pdf_extract_images
- if form_data.pdf_extract_images is not None
- else request.app.state.config.PDF_EXTRACT_IMAGES
+ # RAG settings
+ request.app.state.config.RAG_TEMPLATE = (
+ form_data.TEMPLATE
+ if form_data.TEMPLATE is not None
+ else request.app.state.config.RAG_TEMPLATE
+ )
+ request.app.state.config.TOP_K = (
+ form_data.TOP_K
+ if form_data.TOP_K is not None
+ else request.app.state.config.TOP_K
+ )
+ request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = (
+ form_data.BYPASS_EMBEDDING_AND_RETRIEVAL
+ if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None
+ else request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
)
-
request.app.state.config.RAG_FULL_CONTEXT = (
form_data.RAG_FULL_CONTEXT
if form_data.RAG_FULL_CONTEXT is not None
else request.app.state.config.RAG_FULL_CONTEXT
)
- request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = (
- form_data.BYPASS_EMBEDDING_AND_RETRIEVAL
- if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None
- else request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
- )
-
- request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
- form_data.enable_google_drive_integration
- if form_data.enable_google_drive_integration is not None
- else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
- )
-
- request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
- form_data.enable_onedrive_integration
- if form_data.enable_onedrive_integration is not None
- else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
- )
-
- if form_data.file is not None:
- request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
- request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
-
- if form_data.content_extraction is not None:
- log.info(
- f"Updating content extraction: {request.app.state.config.CONTENT_EXTRACTION_ENGINE} to {form_data.content_extraction.engine}"
- )
- request.app.state.config.CONTENT_EXTRACTION_ENGINE = (
- form_data.content_extraction.engine
- )
- request.app.state.config.TIKA_SERVER_URL = (
- form_data.content_extraction.tika_server_url
- )
- request.app.state.config.DOCLING_SERVER_URL = (
- form_data.content_extraction.docling_server_url
- )
- if form_data.content_extraction.document_intelligence_config is not None:
- request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
- form_data.content_extraction.document_intelligence_config.endpoint
- )
- request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = (
- form_data.content_extraction.document_intelligence_config.key
- )
- if form_data.content_extraction.mistral_ocr_config is not None:
- request.app.state.config.MISTRAL_OCR_API_KEY = (
- form_data.content_extraction.mistral_ocr_config.api_key
- )
-
- if form_data.chunk is not None:
- request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter
- request.app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
- request.app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
-
- if form_data.youtube is not None:
- request.app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
- request.app.state.config.YOUTUBE_LOADER_PROXY_URL = form_data.youtube.proxy_url
- request.app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
-
- if form_data.web is not None:
- 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
- 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.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
-
- request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
- form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
- )
-
- request.app.state.config.SEARXNG_QUERY_URL = (
- form_data.web.search.searxng_query_url
- )
- request.app.state.config.GOOGLE_PSE_API_KEY = (
- form_data.web.search.google_pse_api_key
- )
- request.app.state.config.GOOGLE_PSE_ENGINE_ID = (
- form_data.web.search.google_pse_engine_id
- )
- request.app.state.config.BRAVE_SEARCH_API_KEY = (
- form_data.web.search.brave_search_api_key
- )
- request.app.state.config.KAGI_SEARCH_API_KEY = (
- form_data.web.search.kagi_search_api_key
- )
- request.app.state.config.MOJEEK_SEARCH_API_KEY = (
- form_data.web.search.mojeek_search_api_key
- )
- request.app.state.config.BOCHA_SEARCH_API_KEY = (
- form_data.web.search.bocha_search_api_key
- )
- request.app.state.config.SERPSTACK_API_KEY = (
- form_data.web.search.serpstack_api_key
- )
- request.app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
- request.app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
- request.app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
- request.app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key
- request.app.state.config.SEARCHAPI_API_KEY = (
- form_data.web.search.searchapi_api_key
- )
- request.app.state.config.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.BING_SEARCH_V7_ENDPOINT = (
- form_data.web.search.bing_search_v7_endpoint
- )
- request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = (
- form_data.web.search.bing_search_v7_subscription_key
- )
-
- request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key
-
- request.app.state.config.PERPLEXITY_API_KEY = (
- form_data.web.search.perplexity_api_key
- )
-
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = (
- form_data.web.search.result_count
- )
- request.app.state.config.RAG_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 = (
- form_data.web.search.domain_filter_list
- )
-
- return {
- "status": True,
- "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
- "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
- "BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
- "file": {
- "max_size": request.app.state.config.FILE_MAX_SIZE,
- "max_count": request.app.state.config.FILE_MAX_COUNT,
- },
- "content_extraction": {
- "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
- "tika_server_url": request.app.state.config.TIKA_SERVER_URL,
- "docling_server_url": request.app.state.config.DOCLING_SERVER_URL,
- "document_intelligence_config": {
- "endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
- "key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
- },
- "mistral_ocr_config": {
- "api_key": request.app.state.config.MISTRAL_OCR_API_KEY,
- },
- },
- "chunk": {
- "text_splitter": request.app.state.config.TEXT_SPLITTER,
- "chunk_size": request.app.state.config.CHUNK_SIZE,
- "chunk_overlap": request.app.state.config.CHUNK_OVERLAP,
- },
- "youtube": {
- "language": request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
- "proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
- "translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
- },
- "web": {
- "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
- "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
- "search": {
- "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
- "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
- "searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
- "google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
- "google_pse_engine_id": request.app.state.config.GOOGLE_PSE_ENGINE_ID,
- "brave_search_api_key": request.app.state.config.BRAVE_SEARCH_API_KEY,
- "kagi_search_api_key": request.app.state.config.KAGI_SEARCH_API_KEY,
- "mojeek_search_api_key": request.app.state.config.MOJEEK_SEARCH_API_KEY,
- "bocha_search_api_key": request.app.state.config.BOCHA_SEARCH_API_KEY,
- "serpstack_api_key": request.app.state.config.SERPSTACK_API_KEY,
- "serpstack_https": request.app.state.config.SERPSTACK_HTTPS,
- "serper_api_key": request.app.state.config.SERPER_API_KEY,
- "serply_api_key": request.app.state.config.SERPLY_API_KEY,
- "serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
- "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,
- "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_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
- "exa_api_key": request.app.state.config.EXA_API_KEY,
- "perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
- "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- "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,
- },
- },
- }
-
-
-@router.get("/template")
-async def get_rag_template(request: Request, user=Depends(get_verified_user)):
- return {
- "status": True,
- "template": request.app.state.config.RAG_TEMPLATE,
- }
-
-
-@router.get("/query/settings")
-async def get_query_settings(request: Request, user=Depends(get_admin_user)):
- return {
- "status": True,
- "template": request.app.state.config.RAG_TEMPLATE,
- "k": request.app.state.config.TOP_K,
- "k_reranker": request.app.state.config.TOP_K_RERANKER,
- "r": request.app.state.config.RELEVANCE_THRESHOLD,
- "hybrid": request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
- }
-
-
-class QuerySettingsForm(BaseModel):
- k: Optional[int] = None
- k_reranker: Optional[int] = None
- r: Optional[float] = None
- template: Optional[str] = None
- hybrid: Optional[bool] = None
-
-
-@router.post("/query/settings/update")
-async def update_query_settings(
- request: Request, form_data: QuerySettingsForm, user=Depends(get_admin_user)
-):
- request.app.state.config.RAG_TEMPLATE = form_data.template
- request.app.state.config.TOP_K = form_data.k if form_data.k else 4
- request.app.state.config.TOP_K_RERANKER = form_data.k_reranker or 4
- request.app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
-
+ # Hybrid search settings
request.app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
- form_data.hybrid if form_data.hybrid else False
+ form_data.ENABLE_RAG_HYBRID_SEARCH
+ if form_data.ENABLE_RAG_HYBRID_SEARCH is not None
+ else request.app.state.config.ENABLE_RAG_HYBRID_SEARCH
)
-
+ # Free up memory if hybrid search is disabled
if not request.app.state.config.ENABLE_RAG_HYBRID_SEARCH:
request.app.state.rf = None
+ request.app.state.config.TOP_K_RERANKER = (
+ form_data.TOP_K_RERANKER
+ if form_data.TOP_K_RERANKER is not None
+ else request.app.state.config.TOP_K_RERANKER
+ )
+ request.app.state.config.RELEVANCE_THRESHOLD = (
+ form_data.RELEVANCE_THRESHOLD
+ if form_data.RELEVANCE_THRESHOLD is not None
+ else request.app.state.config.RELEVANCE_THRESHOLD
+ )
+
+ # Content extraction settings
+ request.app.state.config.CONTENT_EXTRACTION_ENGINE = (
+ form_data.CONTENT_EXTRACTION_ENGINE
+ if form_data.CONTENT_EXTRACTION_ENGINE is not None
+ else request.app.state.config.CONTENT_EXTRACTION_ENGINE
+ )
+ request.app.state.config.PDF_EXTRACT_IMAGES = (
+ form_data.PDF_EXTRACT_IMAGES
+ if form_data.PDF_EXTRACT_IMAGES is not None
+ else request.app.state.config.PDF_EXTRACT_IMAGES
+ )
+ request.app.state.config.TIKA_SERVER_URL = (
+ form_data.TIKA_SERVER_URL
+ if form_data.TIKA_SERVER_URL is not None
+ else request.app.state.config.TIKA_SERVER_URL
+ )
+ request.app.state.config.DOCLING_SERVER_URL = (
+ form_data.DOCLING_SERVER_URL
+ if form_data.DOCLING_SERVER_URL is not None
+ else request.app.state.config.DOCLING_SERVER_URL
+ )
+ request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
+ form_data.DOCUMENT_INTELLIGENCE_ENDPOINT
+ if form_data.DOCUMENT_INTELLIGENCE_ENDPOINT is not None
+ else request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT
+ )
+ request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = (
+ form_data.DOCUMENT_INTELLIGENCE_KEY
+ if form_data.DOCUMENT_INTELLIGENCE_KEY is not None
+ else request.app.state.config.DOCUMENT_INTELLIGENCE_KEY
+ )
+ request.app.state.config.MISTRAL_OCR_API_KEY = (
+ form_data.MISTRAL_OCR_API_KEY
+ if form_data.MISTRAL_OCR_API_KEY is not None
+ else request.app.state.config.MISTRAL_OCR_API_KEY
+ )
+
+ # Chunking settings
+ request.app.state.config.TEXT_SPLITTER = (
+ form_data.TEXT_SPLITTER
+ if form_data.TEXT_SPLITTER is not None
+ else request.app.state.config.TEXT_SPLITTER
+ )
+ request.app.state.config.CHUNK_SIZE = (
+ form_data.CHUNK_SIZE
+ if form_data.CHUNK_SIZE is not None
+ else request.app.state.config.CHUNK_SIZE
+ )
+ request.app.state.config.CHUNK_OVERLAP = (
+ form_data.CHUNK_OVERLAP
+ if form_data.CHUNK_OVERLAP is not None
+ else request.app.state.config.CHUNK_OVERLAP
+ )
+
+ # File upload settings
+ request.app.state.config.FILE_MAX_SIZE = (
+ form_data.FILE_MAX_SIZE
+ if form_data.FILE_MAX_SIZE is not None
+ else request.app.state.config.FILE_MAX_SIZE
+ )
+ request.app.state.config.FILE_MAX_COUNT = (
+ form_data.FILE_MAX_COUNT
+ if form_data.FILE_MAX_COUNT is not None
+ else request.app.state.config.FILE_MAX_COUNT
+ )
+
+ # Integration settings
+ request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
+ form_data.ENABLE_GOOGLE_DRIVE_INTEGRATION
+ if form_data.ENABLE_GOOGLE_DRIVE_INTEGRATION is not None
+ else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
+ )
+ request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
+ form_data.ENABLE_ONEDRIVE_INTEGRATION
+ if form_data.ENABLE_ONEDRIVE_INTEGRATION is not None
+ else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
+ )
+
+ if form_data.web is not None:
+ # Web search settings
+ request.app.state.config.ENABLE_WEB_SEARCH = form_data.web.ENABLE_WEB_SEARCH
+ request.app.state.config.WEB_SEARCH_ENGINE = form_data.web.WEB_SEARCH_ENGINE
+ request.app.state.config.WEB_SEARCH_TRUST_ENV = (
+ form_data.web.WEB_SEARCH_TRUST_ENV
+ )
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT = (
+ form_data.web.WEB_SEARCH_RESULT_COUNT
+ )
+ request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = (
+ form_data.web.WEB_SEARCH_CONCURRENT_REQUESTS
+ )
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = (
+ form_data.web.WEB_SEARCH_DOMAIN_FILTER_LIST
+ )
+ request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
+ form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
+ )
+ request.app.state.config.SEARXNG_QUERY_URL = form_data.web.SEARXNG_QUERY_URL
+ request.app.state.config.GOOGLE_PSE_API_KEY = form_data.web.GOOGLE_PSE_API_KEY
+ request.app.state.config.GOOGLE_PSE_ENGINE_ID = (
+ form_data.web.GOOGLE_PSE_ENGINE_ID
+ )
+ request.app.state.config.BRAVE_SEARCH_API_KEY = (
+ form_data.web.BRAVE_SEARCH_API_KEY
+ )
+ request.app.state.config.KAGI_SEARCH_API_KEY = form_data.web.KAGI_SEARCH_API_KEY
+ request.app.state.config.MOJEEK_SEARCH_API_KEY = (
+ form_data.web.MOJEEK_SEARCH_API_KEY
+ )
+ request.app.state.config.BOCHA_SEARCH_API_KEY = (
+ form_data.web.BOCHA_SEARCH_API_KEY
+ )
+ request.app.state.config.SERPSTACK_API_KEY = form_data.web.SERPSTACK_API_KEY
+ request.app.state.config.SERPSTACK_HTTPS = form_data.web.SERPSTACK_HTTPS
+ request.app.state.config.SERPER_API_KEY = form_data.web.SERPER_API_KEY
+ request.app.state.config.SERPLY_API_KEY = form_data.web.SERPLY_API_KEY
+ request.app.state.config.TAVILY_API_KEY = form_data.web.TAVILY_API_KEY
+ request.app.state.config.SEARCHAPI_API_KEY = form_data.web.SEARCHAPI_API_KEY
+ request.app.state.config.SEARCHAPI_ENGINE = form_data.web.SEARCHAPI_ENGINE
+ request.app.state.config.SERPAPI_API_KEY = form_data.web.SERPAPI_API_KEY
+ request.app.state.config.SERPAPI_ENGINE = form_data.web.SERPAPI_ENGINE
+ request.app.state.config.JINA_API_KEY = form_data.web.JINA_API_KEY
+ request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
+ form_data.web.BING_SEARCH_V7_ENDPOINT
+ )
+ request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = (
+ form_data.web.BING_SEARCH_V7_SUBSCRIPTION_KEY
+ )
+ request.app.state.config.EXA_API_KEY = form_data.web.EXA_API_KEY
+ request.app.state.config.PERPLEXITY_API_KEY = form_data.web.PERPLEXITY_API_KEY
+ request.app.state.config.SOUGOU_API_SID = form_data.web.SOUGOU_API_SID
+ request.app.state.config.SOUGOU_API_SK = form_data.web.SOUGOU_API_SK
+
+ # Web loader settings
+ request.app.state.config.WEB_LOADER_ENGINE = form_data.web.WEB_LOADER_ENGINE
+ request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = (
+ form_data.web.ENABLE_WEB_LOADER_SSL_VERIFICATION
+ )
+ request.app.state.config.PLAYWRIGHT_WS_URL = form_data.web.PLAYWRIGHT_WS_URL
+ request.app.state.config.PLAYWRIGHT_TIMEOUT = form_data.web.PLAYWRIGHT_TIMEOUT
+ request.app.state.config.FIRECRAWL_API_KEY = form_data.web.FIRECRAWL_API_KEY
+ request.app.state.config.FIRECRAWL_API_BASE_URL = (
+ form_data.web.FIRECRAWL_API_BASE_URL
+ )
+ request.app.state.config.TAVILY_EXTRACT_DEPTH = (
+ form_data.web.TAVILY_EXTRACT_DEPTH
+ )
+ request.app.state.config.YOUTUBE_LOADER_LANGUAGE = (
+ form_data.web.YOUTUBE_LOADER_LANGUAGE
+ )
+ request.app.state.config.YOUTUBE_LOADER_PROXY_URL = (
+ form_data.web.YOUTUBE_LOADER_PROXY_URL
+ )
+ request.app.state.YOUTUBE_LOADER_TRANSLATION = (
+ form_data.web.YOUTUBE_LOADER_TRANSLATION
+ )
+
return {
"status": True,
- "template": request.app.state.config.RAG_TEMPLATE,
- "k": request.app.state.config.TOP_K,
- "k_reranker": request.app.state.config.TOP_K_RERANKER,
- "r": request.app.state.config.RELEVANCE_THRESHOLD,
- "hybrid": request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
+ "BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
+ "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
+ # Content extraction settings
+ "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
+ "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES,
+ "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
+ "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
+ "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
+ "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
+ "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
+ # Chunking settings
+ "TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER,
+ "CHUNK_SIZE": request.app.state.config.CHUNK_SIZE,
+ "CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP,
+ # File upload settings
+ "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
+ "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
+ # Integration settings
+ "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
+ "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
+ # Web search settings
+ "web": {
+ "ENABLE_WEB_SEARCH": request.app.state.config.ENABLE_WEB_SEARCH,
+ "WEB_SEARCH_ENGINE": request.app.state.config.WEB_SEARCH_ENGINE,
+ "WEB_SEARCH_TRUST_ENV": request.app.state.config.WEB_SEARCH_TRUST_ENV,
+ "WEB_SEARCH_RESULT_COUNT": request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ "WEB_SEARCH_CONCURRENT_REQUESTS": request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS,
+ "WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
+ "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
+ "SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
+ "GOOGLE_PSE_API_KEY": request.app.state.config.GOOGLE_PSE_API_KEY,
+ "GOOGLE_PSE_ENGINE_ID": request.app.state.config.GOOGLE_PSE_ENGINE_ID,
+ "BRAVE_SEARCH_API_KEY": request.app.state.config.BRAVE_SEARCH_API_KEY,
+ "KAGI_SEARCH_API_KEY": request.app.state.config.KAGI_SEARCH_API_KEY,
+ "MOJEEK_SEARCH_API_KEY": request.app.state.config.MOJEEK_SEARCH_API_KEY,
+ "BOCHA_SEARCH_API_KEY": request.app.state.config.BOCHA_SEARCH_API_KEY,
+ "SERPSTACK_API_KEY": request.app.state.config.SERPSTACK_API_KEY,
+ "SERPSTACK_HTTPS": request.app.state.config.SERPSTACK_HTTPS,
+ "SERPER_API_KEY": request.app.state.config.SERPER_API_KEY,
+ "SERPLY_API_KEY": request.app.state.config.SERPLY_API_KEY,
+ "TAVILY_API_KEY": request.app.state.config.TAVILY_API_KEY,
+ "SEARCHAPI_API_KEY": request.app.state.config.SEARCHAPI_API_KEY,
+ "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,
+ "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,
+ "EXA_API_KEY": request.app.state.config.EXA_API_KEY,
+ "PERPLEXITY_API_KEY": request.app.state.config.PERPLEXITY_API_KEY,
+ "SOUGOU_API_SID": request.app.state.config.SOUGOU_API_SID,
+ "SOUGOU_API_SK": request.app.state.config.SOUGOU_API_SK,
+ "WEB_LOADER_ENGINE": request.app.state.config.WEB_LOADER_ENGINE,
+ "ENABLE_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
+ "PLAYWRIGHT_WS_URL": request.app.state.config.PLAYWRIGHT_WS_URL,
+ "PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT,
+ "FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY,
+ "FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL,
+ "TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH,
+ "YOUTUBE_LOADER_LANGUAGE": request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
+ "YOUTUBE_LOADER_PROXY_URL": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
+ "YOUTUBE_LOADER_TRANSLATION": request.app.state.YOUTUBE_LOADER_TRANSLATION,
+ },
}
@@ -1215,8 +1218,8 @@ def process_web(
loader = get_web_loader(
form_data.url,
- verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
- requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+ verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
+ requests_per_second=request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS,
)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
@@ -1267,6 +1270,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
- TAVILY_API_KEY
- EXA_API_KEY
- PERPLEXITY_API_KEY
+ - SOUGOU_API_SID + SOUGOU_API_SK
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
Args:
@@ -1279,8 +1283,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_searxng(
request.app.state.config.SEARXNG_QUERY_URL,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
@@ -1293,8 +1297,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
request.app.state.config.GOOGLE_PSE_API_KEY,
request.app.state.config.GOOGLE_PSE_ENGINE_ID,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception(
@@ -1305,8 +1309,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_brave(
request.app.state.config.BRAVE_SEARCH_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
@@ -1315,8 +1319,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_kagi(
request.app.state.config.KAGI_SEARCH_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No KAGI_SEARCH_API_KEY found in environment variables")
@@ -1325,8 +1329,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_mojeek(
request.app.state.config.MOJEEK_SEARCH_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No MOJEEK_SEARCH_API_KEY found in environment variables")
@@ -1335,8 +1339,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_bocha(
request.app.state.config.BOCHA_SEARCH_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No BOCHA_SEARCH_API_KEY found in environment variables")
@@ -1345,8 +1349,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_serpstack(
request.app.state.config.SERPSTACK_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
https_enabled=request.app.state.config.SERPSTACK_HTTPS,
)
else:
@@ -1356,8 +1360,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_serper(
request.app.state.config.SERPER_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPER_API_KEY found in environment variables")
@@ -1366,24 +1370,24 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_serply(
request.app.state.config.SERPLY_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPLY_API_KEY found in environment variables")
elif engine == "duckduckgo":
return search_duckduckgo(
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "tavily":
if request.app.state.config.TAVILY_API_KEY:
return search_tavily(
request.app.state.config.TAVILY_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No TAVILY_API_KEY found in environment variables")
@@ -1393,8 +1397,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
request.app.state.config.SEARCHAPI_API_KEY,
request.app.state.config.SEARCHAPI_ENGINE,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SEARCHAPI_API_KEY found in environment variables")
@@ -1404,8 +1408,8 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
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,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPAPI_API_KEY found in environment variables")
@@ -1413,7 +1417,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
return search_jina(
request.app.state.config.JINA_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
)
elif engine == "bing":
return search_bing(
@@ -1421,23 +1425,39 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
request.app.state.config.BING_SEARCH_V7_ENDPOINT,
str(DEFAULT_LOCALE),
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "exa":
return search_exa(
request.app.state.config.EXA_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "perplexity":
return search_perplexity(
request.app.state.config.PERPLEXITY_API_KEY,
query,
- request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
- request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
+ elif engine == "sougou":
+ if (
+ request.app.state.config.SOUGOU_API_SID
+ and request.app.state.config.SOUGOU_API_SK
+ ):
+ return search_sougou(
+ request.app.state.config.SOUGOU_API_SID,
+ request.app.state.config.SOUGOU_API_SK,
+ query,
+ request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+ request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
+ )
+ else:
+ raise Exception(
+ "No SOUGOU_API_SID or SOUGOU_API_SK found in environment variables"
+ )
else:
raise Exception("No search engine API key found in environment variables")
@@ -1448,10 +1468,10 @@ async def process_web_search(
):
try:
logging.info(
- f"trying to web search with {request.app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
+ f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.query}"
)
web_results = search_web(
- request, request.app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
+ request, request.app.state.config.WEB_SEARCH_ENGINE, form_data.query
)
except Exception as e:
log.exception(e)
@@ -1467,9 +1487,9 @@ async def process_web_search(
urls = [result.link for result in web_results]
loader = get_web_loader(
urls,
- verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
- requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
- trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
+ verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
+ requests_per_second=request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS,
+ trust_env=request.app.state.config.WEB_SEARCH_TRUST_ENV,
)
docs = await loader.aload()
urls = [
diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py
index 8a98b4e20..318f61398 100644
--- a/backend/open_webui/routers/tools.py
+++ b/backend/open_webui/routers/tools.py
@@ -10,11 +10,11 @@ from open_webui.models.tools import (
ToolUserResponse,
Tools,
)
-from open_webui.utils.plugin import load_tools_module_by_id, replace_imports
+from open_webui.utils.plugin import load_tool_module_by_id, replace_imports
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
-from open_webui.utils.tools import get_tools_specs
+from open_webui.utils.tools import get_tool_specs
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
from open_webui.env import SRC_LOG_LEVELS
@@ -45,7 +45,7 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
)
tools = Tools.get_tools()
- for idx, server in enumerate(request.app.state.TOOL_SERVERS):
+ for server in request.app.state.TOOL_SERVERS:
tools.append(
ToolUserResponse(
**{
@@ -60,7 +60,7 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
.get("description", ""),
},
"access_control": request.app.state.config.TOOL_SERVER_CONNECTIONS[
- idx
+ server["idx"]
]
.get("config", {})
.get("access_control", None),
@@ -137,15 +137,15 @@ async def create_new_tools(
if tools is None:
try:
form_data.content = replace_imports(form_data.content)
- tools_module, frontmatter = load_tools_module_by_id(
+ tool_module, frontmatter = load_tool_module_by_id(
form_data.id, content=form_data.content
)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
- TOOLS[form_data.id] = tools_module
+ TOOLS[form_data.id] = tool_module
- specs = get_tools_specs(TOOLS[form_data.id])
+ specs = get_tool_specs(TOOLS[form_data.id])
tools = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
@@ -226,15 +226,13 @@ async def update_tools_by_id(
try:
form_data.content = replace_imports(form_data.content)
- tools_module, frontmatter = load_tools_module_by_id(
- id, content=form_data.content
- )
+ tool_module, frontmatter = load_tool_module_by_id(id, content=form_data.content)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
- TOOLS[id] = tools_module
+ TOOLS[id] = tool_module
- specs = get_tools_specs(TOOLS[id])
+ specs = get_tool_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
@@ -332,7 +330,7 @@ async def get_tools_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
- tools_module, _ = load_tools_module_by_id(id)
+ tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "Valves"):
@@ -375,7 +373,7 @@ async def update_tools_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
- tools_module, _ = load_tools_module_by_id(id)
+ tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if not hasattr(tools_module, "Valves"):
@@ -431,7 +429,7 @@ async def get_tools_user_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
- tools_module, _ = load_tools_module_by_id(id)
+ tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
@@ -455,7 +453,7 @@ async def update_tools_user_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
- tools_module, _ = load_tools_module_by_id(id)
+ tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py
index d1046bced..781676567 100644
--- a/backend/open_webui/routers/users.py
+++ b/backend/open_webui/routers/users.py
@@ -88,6 +88,7 @@ class ChatPermissions(BaseModel):
file_upload: bool = True
delete: bool = True
edit: bool = True
+ multiple_models: bool = True
temporary: bool = True
temporary_enforced: bool = False
diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py
index 83dd74fff..2c64d4bf7 100644
--- a/backend/open_webui/socket/main.py
+++ b/backend/open_webui/socket/main.py
@@ -9,9 +9,8 @@ from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels
from open_webui.models.chats import Chats
from open_webui.utils.redis import (
- parse_redis_sentinel_url,
get_sentinels_from_env,
- AsyncRedisSentinelManager,
+ get_sentinel_url_from_env,
)
from open_webui.env import (
@@ -38,15 +37,10 @@ log.setLevel(SRC_LOG_LEVELS["SOCKET"])
if WEBSOCKET_MANAGER == "redis":
if WEBSOCKET_SENTINEL_HOSTS:
- redis_config = parse_redis_sentinel_url(WEBSOCKET_REDIS_URL)
- mgr = AsyncRedisSentinelManager(
- WEBSOCKET_SENTINEL_HOSTS.split(","),
- sentinel_port=int(WEBSOCKET_SENTINEL_PORT),
- redis_port=redis_config["port"],
- service=redis_config["service"],
- db=redis_config["db"],
- username=redis_config["username"],
- password=redis_config["password"],
+ mgr = socketio.AsyncRedisManager(
+ get_sentinel_url_from_env(
+ WEBSOCKET_REDIS_URL, WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
+ )
)
else:
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
diff --git a/backend/open_webui/tasks.py b/backend/open_webui/tasks.py
index 2740ecb5a..e575e6885 100644
--- a/backend/open_webui/tasks.py
+++ b/backend/open_webui/tasks.py
@@ -5,16 +5,23 @@ from uuid import uuid4
# A dictionary to keep track of active tasks
tasks: Dict[str, asyncio.Task] = {}
+chat_tasks = {}
-def cleanup_task(task_id: str):
+def cleanup_task(task_id: str, id=None):
"""
Remove a completed or canceled task from the global `tasks` dictionary.
"""
tasks.pop(task_id, None) # Remove the task if it exists
+ # If an ID is provided, remove the task from the chat_tasks dictionary
+ if id and task_id in chat_tasks.get(id, []):
+ chat_tasks[id].remove(task_id)
+ if not chat_tasks[id]: # If no tasks left for this ID, remove the entry
+ chat_tasks.pop(id, None)
-def create_task(coroutine):
+
+def create_task(coroutine, id=None):
"""
Create a new asyncio task and add it to the global task dictionary.
"""
@@ -22,9 +29,15 @@ def create_task(coroutine):
task = asyncio.create_task(coroutine) # Create the task
# Add a done callback for cleanup
- task.add_done_callback(lambda t: cleanup_task(task_id))
-
+ task.add_done_callback(lambda t: cleanup_task(task_id, id))
tasks[task_id] = task
+
+ # If an ID is provided, associate the task with that ID
+ if chat_tasks.get(id):
+ chat_tasks[id].append(task_id)
+ else:
+ chat_tasks[id] = [task_id]
+
return task_id, task
@@ -42,6 +55,13 @@ def list_tasks():
return list(tasks.keys())
+def list_task_ids_by_chat_id(id):
+ """
+ List all tasks associated with a specific ID.
+ """
+ return chat_tasks.get(id, [])
+
+
async def stop_task(task_id: str):
"""
Cancel a running task and remove it from the global task list.
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index badae9906..0f3dc67f5 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -235,46 +235,30 @@ async def chat_completion_tools_handler(
if isinstance(tool_result, str):
tool = tools[tool_function_name]
tool_id = tool.get("tool_id", "")
+
+ tool_name = (
+ f"{tool_id}/{tool_function_name}"
+ if tool_id
+ else f"{tool_function_name}"
+ )
if tool.get("metadata", {}).get("citation", False) or tool.get(
"direct", False
):
-
+ # Citation is enabled for this tool
sources.append(
{
"source": {
- "name": (
- f"TOOL:" + f"{tool_id}/{tool_function_name}"
- if tool_id
- else f"{tool_function_name}"
- ),
+ "name": (f"TOOL:{tool_name}"),
},
- "document": [tool_result, *tool_result_files],
- "metadata": [
- {
- "source": (
- f"TOOL:" + f"{tool_id}/{tool_function_name}"
- if tool_id
- else f"{tool_function_name}"
- )
- }
- ],
+ "document": [tool_result],
+ "metadata": [{"source": (f"TOOL:{tool_name}")}],
}
)
else:
- sources.append(
- {
- "source": {},
- "document": [tool_result, *tool_result_files],
- "metadata": [
- {
- "source": (
- f"TOOL:" + f"{tool_id}/{tool_function_name}"
- if tool_id
- else f"{tool_function_name}"
- )
- }
- ],
- }
+ # Citation is not enabled for this tool
+ body["messages"] = add_or_update_user_message(
+ f"\nTool `{tool_name}` Output: {tool_result}",
+ body["messages"],
)
if (
@@ -897,12 +881,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# If context is not empty, insert it into the messages
if len(sources) > 0:
context_string = ""
- for source_idx, source in enumerate(sources):
+ citated_file_idx = {}
+ for _, source in enumerate(sources, 1):
if "document" in source:
- for doc_idx, doc_context in enumerate(source["document"]):
- context_string += (
- f'