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'{doc_context}\n' - ) + for doc_context, doc_meta in zip( + source["document"], source["metadata"] + ): + file_id = doc_meta.get("file_id") + if file_id not in citated_file_idx: + citated_file_idx[file_id] = len(citated_file_idx) + 1 + context_string += f'{doc_context}\n' context_string = context_string.strip() prompt = get_last_user_message(form_data["messages"]) @@ -1609,6 +1597,9 @@ async def process_chat_response( ) if data: + if "event" in data: + await event_emitter(data.get("event", {})) + if "selected_model_id" in data: model_id = data["selected_model_id"] Chats.upsert_message_to_chat_by_id_and_message_id( @@ -1653,14 +1644,27 @@ async def process_chat_response( ) if tool_call_index is not None: - if ( - len(response_tool_calls) - <= tool_call_index - ): + # Check if the tool call already exists + current_response_tool_call = None + for ( + response_tool_call + ) in response_tool_calls: + if ( + response_tool_call.get("index") + == tool_call_index + ): + current_response_tool_call = ( + response_tool_call + ) + break + + if current_response_tool_call is None: + # Add the new tool call response_tool_calls.append( delta_tool_call ) else: + # Update the existing tool call delta_name = delta_tool_call.get( "function", {} ).get("name") @@ -1671,16 +1675,14 @@ async def process_chat_response( ) if delta_name: - response_tool_calls[ - tool_call_index - ]["function"][ - "name" - ] += delta_name + current_response_tool_call[ + "function" + ]["name"] += delta_name if delta_arguments: - response_tool_calls[ - tool_call_index - ]["function"][ + current_response_tool_call[ + "function" + ][ "arguments" ] += delta_arguments @@ -2243,7 +2245,9 @@ async def process_chat_response( await response.background() # background_tasks.add_task(post_response_handler, response, events) - task_id, _ = create_task(post_response_handler(response, events)) + task_id, _ = create_task( + post_response_handler(response, events), id=metadata["chat_id"] + ) return {"status": True, "task_id": task_id} else: diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index b631c2ae3..95d360bed 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -114,9 +114,12 @@ async def get_all_models(request, user: UserModel = None): for custom_model in custom_models: if custom_model.base_model_id is None: for model in models: - if ( - custom_model.id == model["id"] - or custom_model.id == model["id"].split(":")[0] + if custom_model.id == model["id"] or ( + model.get("owned_by") == "ollama" + and custom_model.id + == model["id"].split(":")[ + 0 + ] # Ollama may return model ids in different formats (e.g., 'llama3' vs. 'llama3:7b') ): if custom_model.is_active: model["name"] = custom_model.name diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index f0746da77..d4e519601 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -68,7 +68,7 @@ def replace_imports(content): return content -def load_tools_module_by_id(tool_id, content=None): +def load_tool_module_by_id(tool_id, content=None): if content is None: tool = Tools.get_tool_by_id(tool_id) diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index baccb16ad..e0a53e73d 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -4,7 +4,7 @@ from redis import asyncio as aioredis from urllib.parse import urlparse -def parse_redis_sentinel_url(redis_url): +def parse_redis_service_url(redis_url): parsed_url = urlparse(redis_url) if parsed_url.scheme != "redis": raise ValueError("Invalid Redis URL scheme. Must be 'redis'.") @@ -20,7 +20,7 @@ def parse_redis_sentinel_url(redis_url): def get_redis_connection(redis_url, redis_sentinels, decode_responses=True): if redis_sentinels: - redis_config = parse_redis_sentinel_url(redis_url) + redis_config = parse_redis_service_url(redis_url) sentinel = redis.sentinel.Sentinel( redis_sentinels, port=redis_config["port"], @@ -45,65 +45,14 @@ def get_sentinels_from_env(sentinel_hosts_env, sentinel_port_env): return [] -class AsyncRedisSentinelManager(socketio.AsyncRedisManager): - def __init__( - self, - sentinel_hosts, - sentinel_port=26379, - redis_port=6379, - service="mymaster", - db=0, - username=None, - password=None, - channel="socketio", - write_only=False, - logger=None, - redis_options=None, - ): - """ - Initialize the Redis Sentinel Manager. - This implementation mostly replicates the __init__ of AsyncRedisManager and - overrides _redis_connect() with a version that uses Redis Sentinel - - :param sentinel_hosts: List of Sentinel hosts - :param sentinel_port: Sentinel Port - :param redis_port: Redis Port (currently unsupported by aioredis!) - :param service: Master service name in Sentinel - :param db: Redis database to use - :param username: Redis username (if any) (currently unsupported by aioredis!) - :param password: Redis password (if any) - :param channel: The channel name on which the server sends and receives - notifications. Must be the same in all the servers. - :param write_only: If set to ``True``, only initialize to emit events. The - default of ``False`` initializes the class for emitting - and receiving. - :param redis_options: additional keyword arguments to be passed to - ``aioredis.from_url()``. - """ - self._sentinels = [(host, sentinel_port) for host in sentinel_hosts] - self._redis_port = redis_port - self._service = service - self._db = db - self._username = username - self._password = password - self._channel = channel - self.redis_options = redis_options or {} - - # connect and call grandparent constructor - self._redis_connect() - super(socketio.AsyncRedisManager, self).__init__( - channel=channel, write_only=write_only, logger=logger - ) - - def _redis_connect(self): - """Establish connections to Redis through Sentinel.""" - sentinel = aioredis.sentinel.Sentinel( - self._sentinels, - port=self._redis_port, - db=self._db, - password=self._password, - **self.redis_options, - ) - - self.redis = sentinel.master_for(self._service) - self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) +def get_sentinel_url_from_env(redis_url, sentinel_hosts_env, sentinel_port_env): + redis_config = parse_redis_service_url(redis_url) + username = redis_config["username"] or "" + password = redis_config["password"] or "" + auth_part = "" + if username or password: + auth_part = f"{username}:{password}@" + hosts_part = ",".join( + f"{host}:{sentinel_port_env}" for host in sentinel_hosts_env.split(",") + ) + return f"redis+sentinel://{auth_part}{hosts_part}/{redis_config['db']}/{redis_config['service']}" diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py index 3a8b4b0a4..66bdb4b3e 100644 --- a/backend/open_webui/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -152,6 +152,8 @@ def rag_template(template: str, context: str, query: str): if template.strip() == "": template = DEFAULT_RAG_TEMPLATE + template = prompt_template(template) + if "[context]" not in template and "{{CONTEXT}}" not in template: log.debug( "WARNING: The RAG template does not contain the '[context]' or '{{CONTEXT}}' placeholder." diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 734c23e1b..b5d916e1d 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -4,19 +4,39 @@ import re import inspect import aiohttp import asyncio +import yaml -from typing import Any, Awaitable, Callable, get_type_hints, Dict, List, Union, Optional +from pydantic import BaseModel +from pydantic.fields import FieldInfo +from typing import ( + Any, + Awaitable, + Callable, + get_type_hints, + get_args, + get_origin, + Dict, + List, + Tuple, + Union, + Optional, + Type, +) from functools import update_wrapper, partial from fastapi import Request from pydantic import BaseModel, Field, create_model -from langchain_core.utils.function_calling import convert_to_openai_function + +from langchain_core.utils.function_calling import ( + convert_to_openai_function as convert_pydantic_model_to_openai_function_spec, +) from open_webui.models.tools import Tools from open_webui.models.users import UserModel -from open_webui.utils.plugin import load_tools_module_by_id +from open_webui.utils.plugin import load_tool_module_by_id +from open_webui.env import AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA import copy @@ -55,7 +75,12 @@ def get_tools( tool_server_connection = ( request.app.state.config.TOOL_SERVER_CONNECTIONS[server_idx] ) - tool_server_data = request.app.state.TOOL_SERVERS[server_idx] + tool_server_data = None + for server in request.app.state.TOOL_SERVERS: + if server["idx"] == server_idx: + tool_server_data = server + break + assert tool_server_data is not None specs = tool_server_data.get("specs", []) for spec in specs: @@ -112,7 +137,7 @@ def get_tools( else: module = request.app.state.TOOLS.get(tool_id, None) if module is None: - module, _ = load_tools_module_by_id(tool_id) + module, _ = load_tool_module_by_id(tool_id) request.app.state.TOOLS[tool_id] = module extra_params["__id__"] = tool_id @@ -233,7 +258,7 @@ def parse_docstring(docstring): return param_descriptions -def function_to_pydantic_model(func: Callable) -> type[BaseModel]: +def convert_function_to_pydantic_model(func: Callable) -> type[BaseModel]: """ Converts a Python function's type hints and docstring to a Pydantic model, including support for nested types, default values, and descriptions. @@ -250,45 +275,57 @@ def function_to_pydantic_model(func: Callable) -> type[BaseModel]: parameters = signature.parameters docstring = func.__doc__ - descriptions = parse_docstring(docstring) - tool_description = parse_description(docstring) + description = parse_description(docstring) + function_descriptions = parse_docstring(docstring) field_defs = {} for name, param in parameters.items(): + type_hint = type_hints.get(name, Any) default_value = param.default if param.default is not param.empty else ... - description = descriptions.get(name, None) - if not description: + + description = function_descriptions.get(name, None) + + if description: + field_defs[name] = type_hint, Field(default_value, description=description) + else: field_defs[name] = type_hint, default_value - continue - field_defs[name] = type_hint, Field(default_value, description=description) model = create_model(func.__name__, **field_defs) - model.__doc__ = tool_description + model.__doc__ = description return model -def get_callable_attributes(tool: object) -> list[Callable]: +def get_functions_from_tool(tool: object) -> list[Callable]: return [ getattr(tool, func) for func in dir(tool) - if callable(getattr(tool, func)) - and not func.startswith("__") - and not inspect.isclass(getattr(tool, func)) + if callable( + getattr(tool, func) + ) # checks if the attribute is callable (a method or function). + and not func.startswith( + "__" + ) # filters out special (dunder) methods like init, str, etc. — these are usually built-in functions of an object that you might not need to use directly. + and not inspect.isclass( + getattr(tool, func) + ) # ensures that the callable is not a class itself, just a method or function. ] -def get_tools_specs(tool_class: object) -> list[dict]: - function_model_list = map( - function_to_pydantic_model, get_callable_attributes(tool_class) +def get_tool_specs(tool_module: object) -> list[dict]: + function_models = map( + convert_function_to_pydantic_model, get_functions_from_tool(tool_module) ) - return [ - convert_to_openai_function(function_model) - for function_model in function_model_list + + specs = [ + convert_pydantic_model_to_openai_function_spec(function_model) + for function_model in function_models ] + return specs + def resolve_schema(schema, components): """ @@ -393,14 +430,21 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]: error = None try: - async with aiohttp.ClientSession() as session: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA) + async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(url, headers=headers) as response: if response.status != 200: error_body = await response.json() raise Exception(error_body) - res = await response.json() + + # Check if URL ends with .yaml or .yml to determine format + if url.lower().endswith((".yaml", ".yml")): + text_content = await response.text() + res = yaml.safe_load(text_content) + else: + res = await response.json() except Exception as err: - print("Error:", err) + log.exception(f"Could not fetch tool server spec from {url}") if isinstance(err, dict) and "detail" in err: error = err["detail"] else: diff --git a/backend/requirements.txt b/backend/requirements.txt index 499eae36d..f0cf262ee 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,7 +3,7 @@ uvicorn[standard]==0.34.0 pydantic==2.10.6 python-multipart==0.0.20 -python-socketio==5.11.3 +python-socketio==5.13.0 python-jose==3.4.0 passlib[bcrypt]==1.7.4 @@ -18,7 +18,7 @@ alembic==1.14.0 peewee==3.17.9 peewee-migrate==1.12.2 psycopg2-binary==2.9.9 -pgvector==0.3.5 +pgvector==0.4.0 PyMySQL==1.1.1 bcrypt==4.3.0 @@ -44,7 +44,7 @@ langchain==0.3.19 langchain-community==0.3.18 fake-useragent==2.1.0 -chromadb==0.6.2 +chromadb==0.6.3 pymilvus==2.5.0 qdrant-client~=1.12.0 opensearch-py==2.8.0 @@ -93,12 +93,12 @@ authlib==1.4.1 black==25.1.0 langfuse==2.44.0 -youtube-transcript-api==0.6.3 +youtube-transcript-api==1.0.3 pytube==15.0.0 extract_msg pydub -duckduckgo-search~=7.3.2 +duckduckgo-search~=8.0.0 ## Google Drive google-api-python-client @@ -113,7 +113,7 @@ pytest-docker~=3.1.1 googleapis-common-protos==1.63.2 google-cloud-storage==2.19.0 -azure-identity==1.20.0 +azure-identity==1.21.0 azure-storage-blob==12.24.1 @@ -123,15 +123,18 @@ ldap3==2.9.1 ## Firecrawl firecrawl-py==1.12.0 +# Sougou API SDK(Tencentcloud SDK) +tencentcloud-sdk-python==3.0.1336 + ## Trace -opentelemetry-api==1.30.0 -opentelemetry-sdk==1.30.0 -opentelemetry-exporter-otlp==1.30.0 -opentelemetry-instrumentation==0.51b0 -opentelemetry-instrumentation-fastapi==0.51b0 -opentelemetry-instrumentation-sqlalchemy==0.51b0 -opentelemetry-instrumentation-redis==0.51b0 -opentelemetry-instrumentation-requests==0.51b0 -opentelemetry-instrumentation-logging==0.51b0 -opentelemetry-instrumentation-httpx==0.51b0 -opentelemetry-instrumentation-aiohttp-client==0.51b0 \ No newline at end of file +opentelemetry-api==1.31.1 +opentelemetry-sdk==1.31.1 +opentelemetry-exporter-otlp==1.31.1 +opentelemetry-instrumentation==0.52b1 +opentelemetry-instrumentation-fastapi==0.52b1 +opentelemetry-instrumentation-sqlalchemy==0.52b1 +opentelemetry-instrumentation-redis==0.52b1 +opentelemetry-instrumentation-requests==0.52b1 +opentelemetry-instrumentation-logging==0.52b1 +opentelemetry-instrumentation-httpx==0.52b1 +opentelemetry-instrumentation-aiohttp-client==0.52b1 diff --git a/backend/start.sh b/backend/start.sh index 671c22ff7..4588e4c34 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -4,8 +4,8 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "$SCRIPT_DIR" || exit # Add conditional Playwright browser installation -if [[ "${RAG_WEB_LOADER_ENGINE,,}" == "playwright" ]]; then - if [[ -z "${PLAYWRIGHT_WS_URI}" ]]; then +if [[ "${WEB_LOADER_ENGINE,,}" == "playwright" ]]; then + if [[ -z "${PLAYWRIGHT_WS_URL}" ]]; then echo "Installing Playwright browsers..." playwright install chromium playwright install-deps chromium @@ -65,4 +65,4 @@ if [ -n "$SPACE_ID" ]; then export WEBUI_URL=${SPACE_HOST} fi -WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' +WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' --workers "${UVICORN_WORKERS:-1}" diff --git a/backend/start_windows.bat b/backend/start_windows.bat index 19f6f123c..8d9aae3ac 100644 --- a/backend/start_windows.bat +++ b/backend/start_windows.bat @@ -7,8 +7,8 @@ SET "SCRIPT_DIR=%~dp0" cd /d "%SCRIPT_DIR%" || exit /b :: Add conditional Playwright browser installation -IF /I "%RAG_WEB_LOADER_ENGINE%" == "playwright" ( - IF "%PLAYWRIGHT_WS_URI%" == "" ( +IF /I "%WEB_LOADER_ENGINE%" == "playwright" ( + IF "%PLAYWRIGHT_WS_URL%" == "" ( echo Installing Playwright browsers... playwright install chromium playwright install-deps chromium @@ -41,5 +41,6 @@ IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " ( :: Execute uvicorn SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" -uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --ws auto +IF "%UVICORN_WORKERS%"=="" SET UVICORN_WORKERS=1 +uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --workers %UVICORN_WORKERS% --ws auto :: For ssl user uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --ssl-keyfile "key.pem" --ssl-certfile "cert.pem" --ws auto diff --git a/docker-compose.playwright.yaml b/docker-compose.playwright.yaml index fe570bed0..fa2b49ff9 100644 --- a/docker-compose.playwright.yaml +++ b/docker-compose.playwright.yaml @@ -6,5 +6,5 @@ services: open-webui: environment: - - 'RAG_WEB_LOADER_ENGINE=playwright' - - 'PLAYWRIGHT_WS_URI=ws://playwright:3000' \ No newline at end of file + - 'WEB_LOADER_ENGINE=playwright' + - 'PLAYWRIGHT_WS_URL=ws://playwright:3000' diff --git a/package-lock.json b/package-lock.json index 360d02a39..250e7b97a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.2", + "version": "0.6.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.2", + "version": "0.6.3", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", @@ -23,12 +23,13 @@ "@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-placeholder": "^2.10.0", "@tiptap/extension-typography": "^2.10.0", - "@tiptap/pm": "^2.10.0", + "@tiptap/pm": "^2.11.7", "@tiptap/starter-kit": "^2.10.0", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.19.7", "codemirror": "^6.0.1", + "codemirror-lang-elixir": "^4.0.0", "codemirror-lang-hcl": "^0.0.0-beta.2", "crc-32": "^1.2.2", "dayjs": "^1.11.10", @@ -69,7 +70,8 @@ "turndown": "^7.2.0", "undici": "^7.3.0", "uuid": "^9.0.1", - "vite-plugin-static-copy": "^2.2.0" + "vite-plugin-static-copy": "^2.2.0", + "yaml": "^2.7.1" }, "devDependencies": { "@sveltejs/adapter-auto": "3.2.2", @@ -3042,9 +3044,9 @@ } }, "node_modules/@tiptap/pm": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.10.0.tgz", - "integrity": "sha512-ohshlWf4MlW6D3rQkNQnhmiQ2w4pwRoQcJmTPt8UJoIDGkeKmZh494fQp4Aeh80XuGd81SsCv//1HJeyaeHJYQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz", + "integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.1", @@ -3061,10 +3063,10 @@ "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.6.1", + "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", - "prosemirror-view": "^1.36.0" + "prosemirror-view": "^1.37.0" }, "funding": { "type": "github", @@ -4625,6 +4627,15 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/codemirror-lang-elixir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/codemirror-lang-elixir/-/codemirror-lang-elixir-4.0.0.tgz", + "integrity": "sha512-mzFesxo/t6KOxwnkqVd34R/q7yk+sMtHh6vUKGAvjwHmpL7bERHB+vQAsmU/nqrndkwVeJEHWGw/z/ybfdiudA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "lezer-elixir": "^1.0.0" + } + }, "node_modules/codemirror-lang-hcl": { "version": "0.0.0-beta.2", "resolved": "https://registry.npmjs.org/codemirror-lang-hcl/-/codemirror-lang-hcl-0.0.0-beta.2.tgz", @@ -7611,6 +7622,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lezer-elixir": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lezer-elixir/-/lezer-elixir-1.1.2.tgz", + "integrity": "sha512-K3yPMJcNhqCL6ugr5NkgOC1g37rcOM38XZezO9lBXy0LwWFd8zdWXfmRbY829vZVk0OGCQoI02yDWp9FF2OWZA==", + "dependencies": { + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.3.0" + } + }, "node_modules/lightningcss": { "version": "1.29.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", @@ -9495,6 +9515,16 @@ "node": ">=10" } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", @@ -9783,9 +9813,10 @@ } }, "node_modules/prosemirror-model": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz", - "integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.0.tgz", + "integrity": "sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==", + "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" } @@ -9819,16 +9850,16 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz", - "integrity": "sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.4.tgz", + "integrity": "sha512-TkDY3Gw52gRFRfRn2f4wJv5WOgAOXLJA2CQJYIJ5+kdFbfj3acR4JUW6LX2e1hiEBiUwvEhzH5a3cZ5YSztpIA==", "license": "MIT", "dependencies": { - "prosemirror-keymap": "^1.1.2", - "prosemirror-model": "^1.8.1", - "prosemirror-state": "^1.3.1", - "prosemirror-transform": "^1.2.1", - "prosemirror-view": "^1.13.3" + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.2" } }, "node_modules/prosemirror-trailing-node": { @@ -9856,9 +9887,9 @@ } }, "node_modules/prosemirror-view": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.36.0.tgz", - "integrity": "sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==", + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.1.tgz", + "integrity": "sha512-GhLxH1xwnqa5VjhJ29LfcQITNDp+f1jzmMPXQfGW9oNrF0lfjPzKvV5y/bjIQkyKpwCX3Fp+GA4dBpMMk8g+ZQ==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", @@ -13011,12 +13042,15 @@ } }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index f670644df..6a2e2e34a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.2", + "version": "0.6.3", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -66,13 +66,14 @@ "@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-placeholder": "^2.10.0", "@tiptap/extension-typography": "^2.10.0", - "@tiptap/pm": "^2.10.0", + "@tiptap/pm": "^2.11.7", "@tiptap/starter-kit": "^2.10.0", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.19.7", "codemirror": "^6.0.1", "codemirror-lang-hcl": "^0.0.0-beta.2", + "codemirror-lang-elixir": "^4.0.0", "crc-32": "^1.2.2", "dayjs": "^1.11.10", "dompurify": "^3.1.6", @@ -112,7 +113,8 @@ "turndown": "^7.2.0", "undici": "^7.3.0", "uuid": "^9.0.1", - "vite-plugin-static-copy": "^2.2.0" + "vite-plugin-static-copy": "^2.2.0", + "yaml": "^2.7.1" }, "engines": { "node": ">=18.13.0 <=22.x.x", diff --git a/pyproject.toml b/pyproject.toml index 52260e45e..8a48c90fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,9 @@ dependencies = [ "fastapi==0.115.7", "uvicorn[standard]==0.34.0", "pydantic==2.10.6", - "python-multipart==0.0.18", + "python-multipart==0.0.20", - "python-socketio==5.11.3", + "python-socketio==5.13.0", "python-jose==3.4.0", "passlib[bcrypt]==1.7.4", @@ -26,7 +26,7 @@ dependencies = [ "peewee==3.17.9", "peewee-migrate==1.12.2", "psycopg2-binary==2.9.9", - "pgvector==0.3.5", + "pgvector==0.4.0", "PyMySQL==1.1.1", "bcrypt==4.3.0", @@ -52,7 +52,7 @@ dependencies = [ "langchain-community==0.3.18", "fake-useragent==2.1.0", - "chromadb==0.6.2", + "chromadb==0.6.3", "pymilvus==2.5.0", "qdrant-client~=1.12.0", "opensearch-py==2.8.0", @@ -99,12 +99,12 @@ dependencies = [ "black==25.1.0", "langfuse==2.44.0", - "youtube-transcript-api==0.6.3", + "youtube-transcript-api==1.0.3", "pytube==15.0.0", "extract_msg", "pydub", - "duckduckgo-search~=7.3.2", + "duckduckgo-search~=8.0.0", "google-api-python-client", "google-auth-httplib2", @@ -113,7 +113,6 @@ dependencies = [ "docker~=7.1.0", "pytest~=8.3.2", "pytest-docker~=3.1.1", - "moto[s3]>=5.0.26", "googleapis-common-protos==1.63.2", "google-cloud-storage==2.19.0", @@ -125,7 +124,12 @@ dependencies = [ "firecrawl-py==1.12.0", + "tencentcloud-sdk-python==3.0.1336", + "gcp-storage-emulator>=2024.8.3", + + "moto[s3]>=5.0.26", + ] readme = "README.md" requires-python = ">= 3.11, < 3.13.0a1" diff --git a/src/app.css b/src/app.css index 86e8438f0..d0bd50ace 100644 --- a/src/app.css +++ b/src/app.css @@ -91,10 +91,6 @@ textarea::placeholder { -webkit-app-region: no-drag; } -iframe { - @apply rounded-lg; -} - li p { display: inline; } @@ -222,6 +218,28 @@ input[type='number'] { -moz-appearance: textfield; /* Firefox */ } +.katex-display { + @apply overflow-y-hidden overflow-x-auto max-w-full; +} + +.katex-display::-webkit-scrollbar { + height: 0.4rem; + width: 0.4rem; +} + +.katex-display:active::-webkit-scrollbar-thumb, +.katex-display:focus::-webkit-scrollbar-thumb, +.katex-display:hover::-webkit-scrollbar-thumb { + visibility: visible; +} +.katex-display::-webkit-scrollbar-thumb { + visibility: hidden; +} + +.katex-display::-webkit-scrollbar-corner { + display: none; +} + .cm-editor { height: 100%; width: 100%; diff --git a/src/app.html b/src/app.html index e64e6d583..d19f3d227 100644 --- a/src/app.html +++ b/src/app.html @@ -9,7 +9,7 @@ - + { return res; }; +export const getTaskIdsByChatId = async (token: string, chat_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${chat_id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getToolServerData = async (token: string, url: string) => { let error = null; @@ -271,8 +304,15 @@ export const getToolServerData = async (token: string, url: string) => { } }) .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); + // Check if URL ends with .yaml or .yml to determine format + if (url.toLowerCase().endsWith('.yaml') || url.toLowerCase().endsWith('.yml')) { + if (!res.ok) throw await res.text(); + const text = await res.text(); + return parse(text); + } else { + if (!res.ok) throw await res.json(); + return res.json(); + } }) .catch((err) => { console.log(err); @@ -305,7 +345,7 @@ export const getToolServersData = async (i18n, servers: object[]) => { .filter((server) => server?.config?.enable) .map(async (server) => { const data = await getToolServerData( - server?.key, + (server?.auth_type ?? 'bearer') === 'bearer' ? server?.key : localStorage.token, server?.url + '/' + (server?.path ?? 'openapi.json') ).catch((err) => { toast.error( diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts index c5fad1323..92fda2a95 100644 --- a/src/lib/apis/knowledge/index.ts +++ b/src/lib/apis/knowledge/index.ts @@ -345,3 +345,31 @@ export const deleteKnowledgeById = async (token: string, id: string) => { return res; }; + +export const reindexKnowledgeFiles = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/reindex`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/retrieval/index.ts b/src/lib/apis/retrieval/index.ts index 31317fe0b..f4b937b68 100644 --- a/src/lib/apis/retrieval/index.ts +++ b/src/lib/apis/retrieval/index.ts @@ -50,9 +50,9 @@ type YoutubeConfigForm = { }; type RAGConfigForm = { - pdf_extract_images?: boolean; - enable_google_drive_integration?: boolean; - enable_onedrive_integration?: boolean; + PDF_EXTRACT_IMAGES?: boolean; + ENABLE_GOOGLE_DRIVE_INTEGRATION?: boolean; + ENABLE_ONEDRIVE_INTEGRATION?: boolean; chunk?: ChunkConfigForm; content_extraction?: ContentExtractConfigForm; web_loader_ssl_verification?: boolean; @@ -89,33 +89,6 @@ export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => return res; }; -export const getRAGTemplate = async (token: string) => { - let error = null; - - const res = await fetch(`${RETRIEVAL_API_BASE_URL}/template`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); - - if (error) { - throw error; - } - - return res?.template ?? ''; -}; - export const getQuerySettings = async (token: string) => { let error = null; diff --git a/src/lib/components/AddServerModal.svelte b/src/lib/components/AddServerModal.svelte index 1ce7369e4..a5f0ca5c7 100644 --- a/src/lib/components/AddServerModal.svelte +++ b/src/lib/components/AddServerModal.svelte @@ -35,7 +35,7 @@ let auth_type = 'bearer'; let key = ''; - let accessControl = null; + let accessControl = {}; let enable = true; diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index d36f4af6c..d097f58f6 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -39,6 +39,9 @@ let STT_ENGINE = ''; let STT_MODEL = ''; let STT_WHISPER_MODEL = ''; + let STT_AZURE_API_KEY = ''; + let STT_AZURE_REGION = ''; + let STT_AZURE_LOCALES = ''; let STT_DEEPGRAM_API_KEY = ''; let STT_WHISPER_MODEL_LOADING = false; @@ -108,7 +111,10 @@ ENGINE: STT_ENGINE, MODEL: STT_MODEL, WHISPER_MODEL: STT_WHISPER_MODEL, - DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY + DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY, + AZURE_API_KEY: STT_AZURE_API_KEY, + AZURE_REGION: STT_AZURE_REGION, + AZURE_LOCALES: STT_AZURE_LOCALES } }); @@ -148,6 +154,9 @@ STT_ENGINE = res.stt.ENGINE; STT_MODEL = res.stt.MODEL; STT_WHISPER_MODEL = res.stt.WHISPER_MODEL; + STT_AZURE_API_KEY = res.stt.AZURE_API_KEY; + STT_AZURE_REGION = res.stt.AZURE_REGION; + STT_AZURE_LOCALES = res.stt.AZURE_LOCALES; STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY; } @@ -180,6 +189,7 @@ + @@ -248,6 +258,37 @@ + {:else if STT_ENGINE === 'azure'} +
+
+ + +
+ +
+ +
+
{$i18n.t('Language Locales')}
+
+
+ +
+
+
+
{:else if STT_ENGINE === ''}
{$i18n.t('STT Model')}
diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index b105ebdb9..2047a07e7 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -13,61 +13,36 @@ updateEmbeddingConfig, getRerankingConfig, updateRerankingConfig, - resetUploadDir, getRAGConfig, updateRAGConfig } from '$lib/apis/retrieval'; - import { knowledge, models } from '$lib/stores'; - import { getKnowledgeBases } from '$lib/apis/knowledge'; - import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files'; + import { reindexKnowledgeFiles } from '$lib/apis/knowledge'; + import { deleteAllFiles } from '$lib/apis/files'; import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + import ReindexKnowledgeFilesConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Switch from '$lib/components/common/Switch.svelte'; import Textarea from '$lib/components/common/Textarea.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; const i18n = getContext('i18n'); - let scanDirLoading = false; let updateEmbeddingModelLoading = false; let updateRerankingModelLoading = false; let showResetConfirm = false; let showResetUploadDirConfirm = false; + let showReindexConfirm = false; let embeddingEngine = ''; let embeddingModel = ''; let embeddingBatchSize = 1; let rerankingModel = ''; - let fileMaxSize = null; - let fileMaxCount = null; - - let contentExtractionEngine = 'default'; - let tikaServerUrl = ''; - let showTikaServerUrl = false; - let doclingServerUrl = ''; - let showDoclingServerUrl = false; - let documentIntelligenceEndpoint = ''; - let documentIntelligenceKey = ''; - let showDocumentIntelligenceConfig = false; - let mistralApiKey = ''; - let showMistralOcrConfig = false; - - let textSplitter = ''; - let chunkSize = 0; - let chunkOverlap = 0; - let pdfExtractImages = true; - - let RAG_FULL_CONTEXT = false; - let BYPASS_EMBEDDING_AND_RETRIEVAL = false; - - let enableGoogleDriveIntegration = false; - let enableOneDriveIntegration = false; - let OpenAIUrl = ''; let OpenAIKey = ''; @@ -82,6 +57,8 @@ hybrid: false }; + let RAGConfig = null; + const embeddingModelUpdateHandler = async () => { if (embeddingEngine === '' && embeddingModel.split('/').length - 1 > 1) { toast.error( @@ -176,65 +153,40 @@ }; const submitHandler = async () => { - if (contentExtractionEngine === 'tika' && tikaServerUrl === '') { + if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') { toast.error($i18n.t('Tika Server URL required.')); return; } - if (contentExtractionEngine === 'docling' && doclingServerUrl === '') { + if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling' && RAGConfig.DOCLING_SERVER_URL === '') { toast.error($i18n.t('Docling Server URL required.')); return; } + if ( - contentExtractionEngine === 'document_intelligence' && - (documentIntelligenceEndpoint === '' || documentIntelligenceKey === '') + RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence' && + (RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT === '' || + RAGConfig.DOCUMENT_INTELLIGENCE_KEY === '') ) { toast.error($i18n.t('Document Intelligence endpoint and key required.')); return; } - if (contentExtractionEngine === 'mistral_ocr' && mistralApiKey === '') { + if ( + RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr' && + RAGConfig.MISTRAL_OCR_API_KEY === '' + ) { toast.error($i18n.t('Mistral OCR API Key required.')); return; } - if (!BYPASS_EMBEDDING_AND_RETRIEVAL) { + if (!RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL) { await embeddingModelUpdateHandler(); - if (querySettings.hybrid) { + if (RAGConfig.ENABLE_RAG_HYBRID_SEARCH) { await rerankingModelUpdateHandler(); } } - const res = await updateRAGConfig(localStorage.token, { - pdf_extract_images: pdfExtractImages, - enable_google_drive_integration: enableGoogleDriveIntegration, - enable_onedrive_integration: enableOneDriveIntegration, - file: { - max_size: fileMaxSize === '' ? null : fileMaxSize, - max_count: fileMaxCount === '' ? null : fileMaxCount - }, - RAG_FULL_CONTEXT: RAG_FULL_CONTEXT, - BYPASS_EMBEDDING_AND_RETRIEVAL: BYPASS_EMBEDDING_AND_RETRIEVAL, - chunk: { - text_splitter: textSplitter, - chunk_overlap: chunkOverlap, - chunk_size: chunkSize - }, - content_extraction: { - engine: contentExtractionEngine, - tika_server_url: tikaServerUrl, - docling_server_url: doclingServerUrl, - document_intelligence_config: { - key: documentIntelligenceKey, - endpoint: documentIntelligenceEndpoint - }, - mistral_ocr_config: { - api_key: mistralApiKey - } - } - }); - - await updateQuerySettings(localStorage.token, querySettings); - + const res = await updateRAGConfig(localStorage.token, RAGConfig); dispatch('save'); }; @@ -262,46 +214,11 @@ } }; - const toggleHybridSearch = async () => { - querySettings = await updateQuerySettings(localStorage.token, querySettings); - }; - onMount(async () => { await setEmbeddingConfig(); await setRerankingConfig(); - querySettings = await getQuerySettings(localStorage.token); - - const res = await getRAGConfig(localStorage.token); - - if (res) { - pdfExtractImages = res.pdf_extract_images; - - textSplitter = res.chunk.text_splitter; - chunkSize = res.chunk.chunk_size; - chunkOverlap = res.chunk.chunk_overlap; - - RAG_FULL_CONTEXT = res.RAG_FULL_CONTEXT; - BYPASS_EMBEDDING_AND_RETRIEVAL = res.BYPASS_EMBEDDING_AND_RETRIEVAL; - - contentExtractionEngine = res.content_extraction.engine; - tikaServerUrl = res.content_extraction.tika_server_url; - doclingServerUrl = res.content_extraction.docling_server_url; - - showTikaServerUrl = contentExtractionEngine === 'tika'; - showDoclingServerUrl = contentExtractionEngine === 'docling'; - documentIntelligenceEndpoint = res.content_extraction.document_intelligence_config.endpoint; - documentIntelligenceKey = res.content_extraction.document_intelligence_config.key; - showDocumentIntelligenceConfig = contentExtractionEngine === 'document_intelligence'; - mistralApiKey = res.content_extraction.mistral_ocr_config.api_key; - showMistralOcrConfig = contentExtractionEngine === 'mistral_ocr'; - - fileMaxSize = res?.file.max_size ?? ''; - fileMaxCount = res?.file.max_count ?? ''; - - enableGoogleDriveIntegration = res.enable_google_drive_integration; - enableOneDriveIntegration = res.enable_onedrive_integration; - } + RAGConfig = await getRAGConfig(localStorage.token); }); @@ -333,345 +250,113 @@ }} /> + { + const res = await reindexKnowledgeFiles(localStorage.token).catch((error) => { + toast.error(`${error}`); + return null; + }); + + if (res) { + toast.success($i18n.t('Success')); + } + }} +/> +
{ submitHandler(); }} > -
-
-
-
{$i18n.t('General')}
- -
- -
-
-
- {$i18n.t('Content Extraction Engine')} -
-
- -
-
- {#if contentExtractionEngine === 'tika'} -
-
- -
-
- {:else if contentExtractionEngine === 'docling'} -
- -
- {:else if contentExtractionEngine === 'document_intelligence'} -
- - -
- {:else if contentExtractionEngine === 'mistral_ocr'} -
- -
- {/if} -
- - {#if contentExtractionEngine === ''} -
-
- {$i18n.t('PDF Extract Images (OCR)')} -
-
- -
-
- {/if} - -
-
- - {$i18n.t('Bypass Embedding and Retrieval')} - -
-
- - - -
-
- - {#if !BYPASS_EMBEDDING_AND_RETRIEVAL} -
-
{$i18n.t('Text Splitter')}
-
- -
-
- -
-
-
-
- {$i18n.t('Chunk Size')} -
-
- -
-
- -
-
- {$i18n.t('Chunk Overlap')} -
- -
- -
-
-
-
- {/if} -
- - {#if !BYPASS_EMBEDDING_AND_RETRIEVAL} + {#if RAGConfig} +
+
-
{$i18n.t('Embedding')}
+
{$i18n.t('General')}

-
+
-
- {$i18n.t('Embedding Model Engine')} +
+ {$i18n.t('Content Extraction Engine')}
-
+
- {#if embeddingEngine === 'openai'} -
- - - + {#if RAGConfig.CONTENT_EXTRACTION_ENGINE === ''} +
+
+
+ {$i18n.t('PDF Extract Images (OCR)')} +
+
+ +
+
- {:else if embeddingEngine === 'ollama'} + {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika'} +
+
+ +
+
+ {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'} +
+ +
+ {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
- +
+ {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'} +
+
{/if}
-
-
{$i18n.t('Embedding Model')}
- -
- {#if embeddingEngine === 'ollama'} -
-
- -
-
- {:else} -
-
- -
- - {#if embeddingEngine === ''} - - {/if} -
- {/if} -
- -
- {$i18n.t( - 'Warning: If you update or change your embedding model, you will need to re-import all documents.' - )} -
-
- - {#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'} -
-
{$i18n.t('Embedding Batch Size')}
- -
- -
-
- {/if} -
- -
-
{$i18n.t('Retrieval')}
- -
-
-
{$i18n.t('Full Context Mode')}
+
+ + {$i18n.t('Bypass Embedding and Retrieval')} + +
- +
- {#if !RAG_FULL_CONTEXT} + {#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
-
{$i18n.t('Hybrid Search')}
+
{$i18n.t('Text Splitter')}
- { - toggleHybridSearch(); - }} - /> +
- {#if querySettings.hybrid === true} -
-
{$i18n.t('Reranking Model')}
+
+
+
+
+ {$i18n.t('Chunk Size')} +
+
+ +
+
-
+
+
+ {$i18n.t('Chunk Overlap')} +
+ +
+ +
+
+
+
+ {/if} +
+ + {#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL} +
+
{$i18n.t('Embedding')}
+ +
+ +
+
+
+ {$i18n.t('Embedding Model Engine')} +
+
+ +
+
+ + {#if embeddingEngine === 'openai'} +
+ + + +
+ {:else if embeddingEngine === 'ollama'} +
+ + + +
+ {/if} +
+ +
+
{$i18n.t('Embedding Model')}
+ +
+ {#if embeddingEngine === 'ollama'}
+
+
+ {:else} +
+
+
- + {/if} + + {/if}
-
+ {/if}
- {/if} -
-
{$i18n.t('Top K')}
-
- +
+ {$i18n.t( + 'Warning: If you update or change your embedding model, you will need to re-import all documents.' + )}
- {#if querySettings.hybrid === true} -
-
{$i18n.t('Top K Reranker')}
+ {#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'} +
+
+ {$i18n.t('Embedding Batch Size')} +
+ +
+ +
+
+ {/if} +
+ +
+
{$i18n.t('Retrieval')}
+ +
+ +
+
{$i18n.t('Full Context Mode')}
+
+ + + +
+
+ + {#if !RAGConfig.RAG_FULL_CONTEXT} +
+
{$i18n.t('Hybrid Search')}
+
+ { + submitHandler(); + }} + /> +
+
+ + {#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true} +
+
{$i18n.t('Reranking Model')}
+ +
+
+
+ +
+ +
+
+
+ {/if} + +
+
{$i18n.t('Top K')}
- {/if} - {#if querySettings.hybrid === true} -
-
-
{$i18n.t('Minimum Score')}
+ {#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true} +
+
{$i18n.t('Top K Reranker')}
-
- {$i18n.t( - 'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.' - )} -
-
- {/if} - {/if} + {/if} -
-
{$i18n.t('RAG Template')}
-
- -