diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a795d515..5a6e9f0098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ 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.13] - 2025-05-30 + +### Added + +- 🟦 **Azure OpenAI Embedding Support**: You can now select Azure OpenAI endpoints for text embeddings, unlocking seamless integration with enterprise-scale Azure AI for powerful RAG and knowledge workflows—no more workarounds, connect and scale effortlessly. +- 🧩 **Smarter Custom Parameter Handling**: Instantly enjoy more flexible model setup—any JSON pasted into custom parameter fields is now parsed automatically, so you can define rich, nested parameters without tedious manual adjustment. This streamlines advanced configuration for all models and accelerates experimentation. +- ⚙️ **General Backend Refactoring**: Significant backend improvements deliver a cleaner codebase for better maintainability, faster performance, and even greater platform reliability—making all your workflows run more smoothly. +- 🌏 **Localization Upgrades**: Experience highly improved user interface translations and clarity in Simplified, Traditional Chinese, Korean, and Finnish, offering a more natural, accurate, and accessible experience for global users. + +### Fixed + +- 🛡️ **Robust Message Handling on Chat Load**: Fixed an issue where chat pages could fail to load if a referenced message was missing or undefined; now, chats always load smoothly and missing IDs no longer disrupt your workflow. +- 📝 **Correct Prompt Access Control**: Ensured that the prompt access controls register properly, restoring reliable permissioning and safeguarding your prompt workflows. +- 🛠 **Open WebUI-Specific Params No Longer Sent to Models**: Fixed a bug that sent internal WebUI parameters to APIs, ensuring only intended model options are transmitted—restoring predictable, error-free model operation. +- 🧠 **Refined Memory Error Handling**: Enhanced stability during memory-related operations, so even uncommon memory errors are gracefully managed without disrupting your session—resulting in a more reliable, worry-free experience. + +## [0.6.12] - 2025-05-29 + +### Added + +- 🧩 **Custom Advanced Model Parameters**: You can now add your own tailor-made advanced parameters to any model, empowering you to fine-tune behavior and unlock greater flexibility beyond just the built-in options—accelerate your experimentation. +- 🪧 **Datalab Marker API Content Extraction Support**: Seamlessly extract content from files and documents using the Datalab Marker API directly in your workflows, enabling more robust structured data extraction for RAG and document processing with just a simple engine switch in the UI. +- ⚡ **Parallelized Base Model Fetching**: Experience noticeably faster startup and model refresh times—base model data now loads in parallel, drastically shortening delays in busy or large-scale deployments. +- 🧠 **Efficient Function Loading and Caching**: Functions are now only reloaded if their content changes, preventing unnecessary duplicate loads, saving bandwidth, and boosting performance. +- 🌍 **Localization & Translation Enhancements**: Improved and expanded Simplified, Traditional Chinese, and Russian translations, providing smoother, more accurate, and context-aware experiences for global users. + +### Fixed + +- 💬 **Stable Message Input Box**: Fixed an issue where the message input box would shift unexpectedly (especially on mobile or with screen reader support), ensuring a smooth and reliable typing experience for every user. +- 🔊 **Reliable Read Aloud (Text-to-Speech)**: Read aloud now works seamlessly across messages, so users depending on TTS for accessibility or multitasking will experience uninterrupted and clear voice playback. +- 🖼 **Image Preview and Download Restored**: Fixed problems with image preview and downloads, ensuring frictionless creation, previewing, and downloading of images in your chats—no more interruptions in creative or documentation workflows. +- 📱 **Improved Mobile Styling for Workspace Capabilities**: Capabilities management is now readable and easy-to-use even on mobile devices, empowering admins and users to manage access quickly on the go. +- 🔁 **/api/v1/retrieval/query/collection Endpoint Reliability**: Queries to retrieval collections now return the expected results, bolstering the reliability of your knowledge workflows and citation-ready responses. + +### Removed + +- 🧹 **Duplicate CSS Elements**: Streamlined the UI by removing redundant CSS, reducing clutter and improving load times for a smoother visual experience. + ## [0.6.11] - 2025-05-27 ### Added diff --git a/README.md b/README.md index 8445b5a392..2ad208c3f7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,17 @@ Want to learn more about Open WebUI's features? Check out our [Open WebUI docume - Does your interface have a backend yet?
Try n8n + N8N • Does your interface have a backend yet?
Try n8n + + + + + + n8n + + + + Warp • The intelligent terminal for developers diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 441c99efbf..0f49483610 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1848,6 +1848,61 @@ CONTENT_EXTRACTION_ENGINE = PersistentConfig( os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(), ) +DATALAB_MARKER_API_KEY = PersistentConfig( + "DATALAB_MARKER_API_KEY", + "rag.datalab_marker_api_key", + os.environ.get("DATALAB_MARKER_API_KEY", ""), +) + +DATALAB_MARKER_LANGS = PersistentConfig( + "DATALAB_MARKER_LANGS", + "rag.datalab_marker_langs", + os.environ.get("DATALAB_MARKER_LANGS", ""), +) + +DATALAB_MARKER_USE_LLM = PersistentConfig( + "DATALAB_MARKER_USE_LLM", + "rag.DATALAB_MARKER_USE_LLM", + os.environ.get("DATALAB_MARKER_USE_LLM", "false").lower() == "true", +) + +DATALAB_MARKER_SKIP_CACHE = PersistentConfig( + "DATALAB_MARKER_SKIP_CACHE", + "rag.datalab_marker_skip_cache", + os.environ.get("DATALAB_MARKER_SKIP_CACHE", "false").lower() == "true", +) + +DATALAB_MARKER_FORCE_OCR = PersistentConfig( + "DATALAB_MARKER_FORCE_OCR", + "rag.datalab_marker_force_ocr", + os.environ.get("DATALAB_MARKER_FORCE_OCR", "false").lower() == "true", +) + +DATALAB_MARKER_PAGINATE = PersistentConfig( + "DATALAB_MARKER_PAGINATE", + "rag.datalab_marker_paginate", + os.environ.get("DATALAB_MARKER_PAGINATE", "false").lower() == "true", +) + +DATALAB_MARKER_STRIP_EXISTING_OCR = PersistentConfig( + "DATALAB_MARKER_STRIP_EXISTING_OCR", + "rag.datalab_marker_strip_existing_ocr", + os.environ.get("DATALAB_MARKER_STRIP_EXISTING_OCR", "false").lower() == "true", +) + +DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = PersistentConfig( + "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", + "rag.datalab_marker_disable_image_extraction", + os.environ.get("DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", "false").lower() + == "true", +) + +DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig( + "DATALAB_MARKER_OUTPUT_FORMAT", + "rag.datalab_marker_output_format", + os.environ.get("DATALAB_MARKER_OUTPUT_FORMAT", "markdown"), +) + EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig( "EXTERNAL_DOCUMENT_LOADER_URL", "rag.external_document_loader_url", @@ -2129,6 +2184,22 @@ RAG_OPENAI_API_KEY = PersistentConfig( os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY), ) +RAG_AZURE_OPENAI_BASE_URL = PersistentConfig( + "RAG_AZURE_OPENAI_BASE_URL", + "rag.azure_openai.base_url", + os.getenv("RAG_AZURE_OPENAI_BASE_URL", ""), +) +RAG_AZURE_OPENAI_API_KEY = PersistentConfig( + "RAG_AZURE_OPENAI_API_KEY", + "rag.azure_openai.api_key", + os.getenv("RAG_AZURE_OPENAI_API_KEY", ""), +) +RAG_AZURE_OPENAI_API_VERSION = PersistentConfig( + "RAG_AZURE_OPENAI_API_VERSION", + "rag.azure_openai.api_version", + os.getenv("RAG_AZURE_OPENAI_API_VERSION", ""), +) + RAG_OLLAMA_BASE_URL = PersistentConfig( "RAG_OLLAMA_BASE_URL", "rag.ollama.url", diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index aa7dbccf95..20fabb2dc7 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -28,7 +28,10 @@ from open_webui.socket.main import ( from open_webui.models.functions import Functions from open_webui.models.models import Models -from open_webui.utils.plugin import load_function_module_by_id +from open_webui.utils.plugin import ( + load_function_module_by_id, + get_function_module_from_cache, +) from open_webui.utils.tools import get_tools from open_webui.utils.access_control import has_access @@ -53,9 +56,7 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"]) def get_function_module_by_id(request: Request, pipe_id: str): - # Check if function is already loaded - function_module, _, _ = load_function_module_by_id(pipe_id) - request.app.state.FUNCTIONS[pipe_id] = function_module + function_module, _, _ = get_function_module_from_cache(request, pipe_id) if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): valves = Functions.get_function_valves_by_id(pipe_id) @@ -252,8 +253,13 @@ async def generate_function_chat_completion( form_data["model"] = model_info.base_model_id params = model_info.params.model_dump() - form_data = apply_model_params_to_body_openai(params, form_data) - form_data = apply_model_system_prompt_to_body(params, form_data, metadata, user) + + if params: + system = params.pop("system", None) + form_data = apply_model_params_to_body_openai(params, form_data) + form_data = apply_model_system_prompt_to_body( + system, form_data, metadata, user + ) pipe_id = get_pipe_id(form_data) function_module = get_function_module_by_id(request, pipe_id) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 999993e84b..6bdcf4957a 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -207,11 +207,23 @@ from open_webui.config import ( RAG_FILE_MAX_SIZE, RAG_OPENAI_API_BASE_URL, RAG_OPENAI_API_KEY, + RAG_AZURE_OPENAI_BASE_URL, + RAG_AZURE_OPENAI_API_KEY, + RAG_AZURE_OPENAI_API_VERSION, RAG_OLLAMA_BASE_URL, RAG_OLLAMA_API_KEY, CHUNK_OVERLAP, CHUNK_SIZE, CONTENT_EXTRACTION_ENGINE, + DATALAB_MARKER_API_KEY, + DATALAB_MARKER_LANGS, + DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_OUTPUT_FORMAT, + DATALAB_MARKER_USE_LLM, EXTERNAL_DOCUMENT_LOADER_URL, EXTERNAL_DOCUMENT_LOADER_API_KEY, TIKA_SERVER_URL, @@ -637,8 +649,12 @@ app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL app.state.USER_COUNT = None + app.state.TOOLS = {} +app.state.TOOL_CONTENTS = {} + app.state.FUNCTIONS = {} +app.state.FUNCTION_CONTENTS = {} ######################################## # @@ -662,6 +678,17 @@ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH 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.DATALAB_MARKER_API_KEY = DATALAB_MARKER_API_KEY +app.state.config.DATALAB_MARKER_LANGS = DATALAB_MARKER_LANGS +app.state.config.DATALAB_MARKER_SKIP_CACHE = DATALAB_MARKER_SKIP_CACHE +app.state.config.DATALAB_MARKER_FORCE_OCR = DATALAB_MARKER_FORCE_OCR +app.state.config.DATALAB_MARKER_PAGINATE = DATALAB_MARKER_PAGINATE +app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = DATALAB_MARKER_STRIP_EXISTING_OCR +app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = ( + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION +) +app.state.config.DATALAB_MARKER_USE_LLM = DATALAB_MARKER_USE_LLM +app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = DATALAB_MARKER_OUTPUT_FORMAT app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL @@ -693,6 +720,10 @@ app.state.config.RAG_TEMPLATE = RAG_TEMPLATE app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL app.state.config.RAG_OPENAI_API_KEY = RAG_OPENAI_API_KEY +app.state.config.RAG_AZURE_OPENAI_BASE_URL = RAG_AZURE_OPENAI_BASE_URL +app.state.config.RAG_AZURE_OPENAI_API_KEY = RAG_AZURE_OPENAI_API_KEY +app.state.config.RAG_AZURE_OPENAI_API_VERSION = RAG_AZURE_OPENAI_API_VERSION + app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY @@ -787,14 +818,27 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( ( app.state.config.RAG_OPENAI_API_BASE_URL if app.state.config.RAG_EMBEDDING_ENGINE == "openai" - else app.state.config.RAG_OLLAMA_BASE_URL + else ( + app.state.config.RAG_OLLAMA_BASE_URL + if app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + else app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) ), ( app.state.config.RAG_OPENAI_API_KEY if app.state.config.RAG_EMBEDDING_ENGINE == "openai" - else app.state.config.RAG_OLLAMA_API_KEY + else ( + app.state.config.RAG_OLLAMA_API_KEY + if app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + else app.state.config.RAG_AZURE_OPENAI_API_KEY + ) ), app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + app.state.config.RAG_AZURE_OPENAI_API_VERSION + if app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" + else None + ), ) ######################################## diff --git a/backend/open_webui/retrieval/loaders/datalab_marker.py b/backend/open_webui/retrieval/loaders/datalab_marker.py new file mode 100644 index 0000000000..104c2830df --- /dev/null +++ b/backend/open_webui/retrieval/loaders/datalab_marker.py @@ -0,0 +1,251 @@ +import os +import time +import requests +import logging +import json +from typing import List, Optional +from langchain_core.documents import Document +from fastapi import HTTPException, status + +log = logging.getLogger(__name__) + + +class DatalabMarkerLoader: + def __init__( + self, + file_path: str, + api_key: str, + langs: Optional[str] = None, + use_llm: bool = False, + skip_cache: bool = False, + force_ocr: bool = False, + paginate: bool = False, + strip_existing_ocr: bool = False, + disable_image_extraction: bool = False, + output_format: str = None, + ): + self.file_path = file_path + self.api_key = api_key + self.langs = langs + self.use_llm = use_llm + self.skip_cache = skip_cache + self.force_ocr = force_ocr + self.paginate = paginate + self.strip_existing_ocr = strip_existing_ocr + self.disable_image_extraction = disable_image_extraction + self.output_format = output_format + + def _get_mime_type(self, filename: str) -> str: + ext = filename.rsplit(".", 1)[-1].lower() + mime_map = { + "pdf": "application/pdf", + "xls": "application/vnd.ms-excel", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "doc": "application/msword", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "odt": "application/vnd.oasis.opendocument.text", + "ppt": "application/vnd.ms-powerpoint", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "odp": "application/vnd.oasis.opendocument.presentation", + "html": "text/html", + "epub": "application/epub+zip", + "png": "image/png", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "webp": "image/webp", + "gif": "image/gif", + "tiff": "image/tiff", + } + return mime_map.get(ext, "application/octet-stream") + + def check_marker_request_status(self, request_id: str) -> dict: + url = f"https://www.datalab.to/api/v1/marker/{request_id}" + headers = {"X-Api-Key": self.api_key} + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + result = response.json() + log.info(f"Marker API status check for request {request_id}: {result}") + return result + except requests.HTTPError as e: + log.error(f"Error checking Marker request status: {e}") + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"Failed to check Marker request: {e}", + ) + except ValueError as e: + log.error(f"Invalid JSON checking Marker request: {e}") + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, detail=f"Invalid JSON: {e}" + ) + + def load(self) -> List[Document]: + url = "https://www.datalab.to/api/v1/marker" + filename = os.path.basename(self.file_path) + mime_type = self._get_mime_type(filename) + headers = {"X-Api-Key": self.api_key} + + form_data = { + "langs": self.langs, + "use_llm": str(self.use_llm).lower(), + "skip_cache": str(self.skip_cache).lower(), + "force_ocr": str(self.force_ocr).lower(), + "paginate": str(self.paginate).lower(), + "strip_existing_ocr": str(self.strip_existing_ocr).lower(), + "disable_image_extraction": str(self.disable_image_extraction).lower(), + "output_format": self.output_format, + } + + log.info( + f"Datalab Marker POST request parameters: {{'filename': '{filename}', 'mime_type': '{mime_type}', **{form_data}}}" + ) + + try: + with open(self.file_path, "rb") as f: + files = {"file": (filename, f, mime_type)} + response = requests.post( + url, data=form_data, files=files, headers=headers + ) + response.raise_for_status() + result = response.json() + except FileNotFoundError: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}" + ) + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Datalab Marker request failed: {e}", + ) + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, detail=f"Invalid JSON response: {e}" + ) + except Exception as e: + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) + + if not result.get("success"): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Datalab Marker request failed: {result.get('error', 'Unknown error')}", + ) + + check_url = result.get("request_check_url") + request_id = result.get("request_id") + if not check_url: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, detail="No request_check_url returned." + ) + + for _ in range(300): # Up to 10 minutes + time.sleep(2) + try: + poll_response = requests.get(check_url, headers=headers) + poll_response.raise_for_status() + poll_result = poll_response.json() + except (requests.HTTPError, ValueError) as e: + raw_body = poll_response.text + log.error(f"Polling error: {e}, response body: {raw_body}") + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, detail=f"Polling failed: {e}" + ) + + status_val = poll_result.get("status") + success_val = poll_result.get("success") + + if status_val == "complete": + summary = { + k: poll_result.get(k) + for k in ( + "status", + "output_format", + "success", + "error", + "page_count", + "total_cost", + ) + } + log.info( + f"Marker processing completed successfully: {json.dumps(summary, indent=2)}" + ) + break + + if status_val == "failed" or success_val is False: + log.error( + f"Marker poll failed full response: {json.dumps(poll_result, indent=2)}" + ) + error_msg = ( + poll_result.get("error") + or "Marker returned failure without error message" + ) + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Marker processing failed: {error_msg}", + ) + else: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, detail="Marker processing timed out" + ) + + if not poll_result.get("success", False): + error_msg = poll_result.get("error") or "Unknown processing error" + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Final processing failed: {error_msg}", + ) + + content_key = self.output_format.lower() + raw_content = poll_result.get(content_key) + + if content_key == "json": + full_text = json.dumps(raw_content, indent=2) + elif content_key in {"markdown", "html"}: + full_text = str(raw_content).strip() + else: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported output format: {self.output_format}", + ) + + if not full_text: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail="Datalab Marker returned empty content", + ) + + marker_output_dir = os.path.join("/app/backend/data/uploads", "marker_output") + os.makedirs(marker_output_dir, exist_ok=True) + + file_ext_map = {"markdown": "md", "json": "json", "html": "html"} + file_ext = file_ext_map.get(content_key, "txt") + output_filename = f"{os.path.splitext(filename)[0]}.{file_ext}" + output_path = os.path.join(marker_output_dir, output_filename) + + try: + with open(output_path, "w", encoding="utf-8") as f: + f.write(full_text) + log.info(f"Saved Marker output to: {output_path}") + except Exception as e: + log.warning(f"Failed to write marker output to disk: {e}") + + metadata = { + "source": filename, + "output_format": poll_result.get("output_format", self.output_format), + "page_count": poll_result.get("page_count", 0), + "processed_with_llm": self.use_llm, + "request_id": request_id or "", + } + + images = poll_result.get("images", {}) + if images: + metadata["image_count"] = len(images) + metadata["images"] = json.dumps(list(images.keys())) + + for k, v in metadata.items(): + if isinstance(v, (dict, list)): + metadata[k] = json.dumps(v) + elif v is None: + metadata[k] = "" + + return [Document(page_content=full_text, metadata=metadata)] diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 22397b3b4a..0d0ff851b7 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -21,9 +21,11 @@ from langchain_community.document_loaders import ( ) from langchain_core.documents import Document - from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader + from open_webui.retrieval.loaders.mistral import MistralLoader +from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader + from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL @@ -236,6 +238,49 @@ class Loader: mime_type=file_content_type, extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"), ) + elif ( + self.engine == "datalab_marker" + and self.kwargs.get("DATALAB_MARKER_API_KEY") + and file_ext + in [ + "pdf", + "xls", + "xlsx", + "ods", + "doc", + "docx", + "odt", + "ppt", + "pptx", + "odp", + "html", + "epub", + "png", + "jpeg", + "jpg", + "webp", + "gif", + "tiff", + ] + ): + loader = DatalabMarkerLoader( + file_path=file_path, + api_key=self.kwargs["DATALAB_MARKER_API_KEY"], + langs=self.kwargs.get("DATALAB_MARKER_LANGS"), + use_llm=self.kwargs.get("DATALAB_MARKER_USE_LLM", False), + skip_cache=self.kwargs.get("DATALAB_MARKER_SKIP_CACHE", False), + force_ocr=self.kwargs.get("DATALAB_MARKER_FORCE_OCR", False), + paginate=self.kwargs.get("DATALAB_MARKER_PAGINATE", False), + strip_existing_ocr=self.kwargs.get( + "DATALAB_MARKER_STRIP_EXISTING_OCR", False + ), + disable_image_extraction=self.kwargs.get( + "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", False + ), + output_format=self.kwargs.get( + "DATALAB_MARKER_OUTPUT_FORMAT", "markdown" + ), + ) elif self.engine == "docling" and self.kwargs.get("DOCLING_SERVER_URL"): if self._is_text_file(file_ext, file_content_type): loader = TextLoader(file_path, autodetect_encoding=True) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 97a89880c5..683f42819b 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -5,6 +5,7 @@ from typing import Optional, Union import requests import hashlib from concurrent.futures import ThreadPoolExecutor +import time from huggingface_hub import snapshot_download from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever @@ -400,12 +401,13 @@ def get_embedding_function( url, key, embedding_batch_size, + azure_api_version=None, ): if embedding_engine == "": return lambda query, prefix=None, user=None: embedding_function.encode( query, **({"prompt": prefix} if prefix else {}) ).tolist() - elif embedding_engine in ["ollama", "openai"]: + elif embedding_engine in ["ollama", "openai", "azure_openai"]: func = lambda query, prefix=None, user=None: generate_embeddings( engine=embedding_engine, model=embedding_model, @@ -414,6 +416,7 @@ def get_embedding_function( url=url, key=key, user=user, + azure_api_version=azure_api_version, ) def generate_multiple(query, prefix, user, func): @@ -697,6 +700,60 @@ def generate_openai_batch_embeddings( return None +def generate_azure_openai_batch_embeddings( + model: str, + texts: list[str], + url: str, + key: str = "", + version: str = "", + prefix: str = None, + user: UserModel = None, +) -> Optional[list[list[float]]]: + try: + log.debug( + f"generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}" + ) + json_data = {"input": texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + url = f"{url}/openai/deployments/{model}/embeddings?api-version={version}" + + for _ in range(5): + r = requests.post( + url, + headers={ + "Content-Type": "application/json", + "api-key": key, + **( + { + "X-OpenWebUI-User-Name": user.name, + "X-OpenWebUI-User-Id": user.id, + "X-OpenWebUI-User-Email": user.email, + "X-OpenWebUI-User-Role": user.role, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user + else {} + ), + }, + json=json_data, + ) + if r.status_code == 429: + retry = float(r.headers.get("Retry-After", "1")) + time.sleep(retry) + continue + r.raise_for_status() + data = r.json() + if "data" in data: + return [elem["embedding"] for elem in data["data"]] + else: + raise Exception("Something went wrong :/") + return None + except Exception as e: + log.exception(f"Error generating azure openai batch embeddings: {e}") + return None + + def generate_ollama_batch_embeddings( model: str, texts: list[str], @@ -761,38 +818,33 @@ def generate_embeddings( text = f"{prefix}{text}" if engine == "ollama": - if isinstance(text, list): - embeddings = generate_ollama_batch_embeddings( - **{ - "model": model, - "texts": text, - "url": url, - "key": key, - "prefix": prefix, - "user": user, - } - ) - else: - embeddings = generate_ollama_batch_embeddings( - **{ - "model": model, - "texts": [text], - "url": url, - "key": key, - "prefix": prefix, - "user": user, - } - ) + embeddings = generate_ollama_batch_embeddings( + **{ + "model": model, + "texts": text if isinstance(text, list) else [text], + "url": url, + "key": key, + "prefix": prefix, + "user": user, + } + ) return embeddings[0] if isinstance(text, str) else embeddings elif engine == "openai": - if isinstance(text, list): - embeddings = generate_openai_batch_embeddings( - model, text, url, key, prefix, user - ) - else: - embeddings = generate_openai_batch_embeddings( - model, [text], url, key, prefix, user - ) + embeddings = generate_openai_batch_embeddings( + model, text if isinstance(text, list) else [text], url, key, prefix, user + ) + return embeddings[0] if isinstance(text, str) else embeddings + elif engine == "azure_openai": + azure_api_version = kwargs.get("azure_api_version", "") + embeddings = generate_azure_openai_batch_embeddings( + model, + text if isinstance(text, list) else [text], + url, + key, + azure_api_version, + prefix, + user, + ) return embeddings[0] if isinstance(text, str) else embeddings diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index d337ece2e3..eac5839d96 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -96,12 +96,9 @@ def is_audio_conversion_required(file_path): # File is AAC/mp4a audio, recommend mp3 conversion return True - # If the codec name or file extension is in the supported formats - if ( - codec_name in SUPPORTED_FORMATS - or os.path.splitext(file_path)[1][1:].lower() in SUPPORTED_FORMATS - ): - return False # Already supported + # If the codec name is in the supported formats + if codec_name in SUPPORTED_FORMATS: + return False return True except Exception as e: diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 2748fa95ce..355093335a 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -12,7 +12,11 @@ from open_webui.models.functions import ( FunctionResponse, Functions, ) -from open_webui.utils.plugin import load_function_module_by_id, replace_imports +from open_webui.utils.plugin import ( + load_function_module_by_id, + replace_imports, + get_function_module_from_cache, +) from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES from fastapi import APIRouter, Depends, HTTPException, Request, status @@ -358,8 +362,9 @@ async def get_function_valves_spec_by_id( ): function = Functions.get_function_by_id(id) if function: - function_module, function_type, frontmatter = load_function_module_by_id(id) - request.app.state.FUNCTIONS[id] = function_module + function_module, function_type, frontmatter = get_function_module_from_cache( + request, id + ) if hasattr(function_module, "Valves"): Valves = function_module.Valves @@ -383,8 +388,9 @@ async def update_function_valves_by_id( ): function = Functions.get_function_by_id(id) if function: - function_module, function_type, frontmatter = load_function_module_by_id(id) - request.app.state.FUNCTIONS[id] = function_module + function_module, function_type, frontmatter = get_function_module_from_cache( + request, id + ) if hasattr(function_module, "Valves"): Valves = function_module.Valves @@ -443,8 +449,9 @@ async def get_function_user_valves_spec_by_id( ): function = Functions.get_function_by_id(id) if function: - function_module, function_type, frontmatter = load_function_module_by_id(id) - request.app.state.FUNCTIONS[id] = function_module + function_module, function_type, frontmatter = get_function_module_from_cache( + request, id + ) if hasattr(function_module, "UserValves"): UserValves = function_module.UserValves @@ -464,8 +471,9 @@ async def update_function_user_valves_by_id( function = Functions.get_function_by_id(id) if function: - function_module, function_type, frontmatter = load_function_module_by_id(id) - request.app.state.FUNCTIONS[id] = function_module + function_module, function_type, frontmatter = get_function_module_from_cache( + request, id + ) if hasattr(function_module, "UserValves"): UserValves = function_module.UserValves diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 1410831d79..95f48fb1c8 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -1283,13 +1283,14 @@ async def generate_chat_completion( params = model_info.params.model_dump() if params: - if payload.get("options") is None: - payload["options"] = {} + system = params.pop("system", None) + # Unlike OpenAI, Ollama does not support params directly in the body payload["options"] = apply_model_params_to_body_ollama( - params, payload["options"] + params, (payload.get("options", {}) or {}) ) - payload = apply_model_system_prompt_to_body(params, payload, metadata, user) + + payload = apply_model_system_prompt_to_body(system, payload, metadata, user) # Check if user has access to the model if not bypass_filter and user.role == "user": @@ -1471,8 +1472,10 @@ async def generate_openai_chat_completion( params = model_info.params.model_dump() if params: + system = params.pop("system", None) + payload = apply_model_params_to_body_openai(params, payload) - payload = apply_model_system_prompt_to_body(params, payload, metadata, user) + payload = apply_model_system_prompt_to_body(system, payload, metadata, user) # Check if user has access to the model if user.role == "user": diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 96c21f9c03..9c3c393677 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -715,8 +715,12 @@ async def generate_chat_completion( model_id = model_info.base_model_id params = model_info.params.model_dump() - payload = apply_model_params_to_body_openai(params, payload) - payload = apply_model_system_prompt_to_body(params, payload, metadata, user) + + if params: + system = params.pop("system", None) + + payload = apply_model_params_to_body_openai(params, payload) + payload = apply_model_system_prompt_to_body(system, payload, metadata, user) # Check if user has access to the model if not bypass_filter and user.role == "user": diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 98f79c7fee..343b0513c9 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -239,6 +239,11 @@ async def get_embedding_config(request: Request, user=Depends(get_admin_user)): "url": request.app.state.config.RAG_OLLAMA_BASE_URL, "key": request.app.state.config.RAG_OLLAMA_API_KEY, }, + "azure_openai_config": { + "url": request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, + "key": request.app.state.config.RAG_AZURE_OPENAI_API_KEY, + "version": request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, + }, } @@ -252,9 +257,16 @@ class OllamaConfigForm(BaseModel): key: str +class AzureOpenAIConfigForm(BaseModel): + url: str + key: str + version: str + + class EmbeddingModelUpdateForm(BaseModel): openai_config: Optional[OpenAIConfigForm] = None ollama_config: Optional[OllamaConfigForm] = None + azure_openai_config: Optional[AzureOpenAIConfigForm] = None embedding_engine: str embedding_model: str embedding_batch_size: Optional[int] = 1 @@ -271,7 +283,11 @@ async def update_embedding_config( request.app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine request.app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model - if request.app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]: + if request.app.state.config.RAG_EMBEDDING_ENGINE in [ + "ollama", + "openai", + "azure_openai", + ]: if form_data.openai_config is not None: request.app.state.config.RAG_OPENAI_API_BASE_URL = ( form_data.openai_config.url @@ -288,6 +304,17 @@ async def update_embedding_config( form_data.ollama_config.key ) + if form_data.azure_openai_config is not None: + request.app.state.config.RAG_AZURE_OPENAI_BASE_URL = ( + form_data.azure_openai_config.url + ) + request.app.state.config.RAG_AZURE_OPENAI_API_KEY = ( + form_data.azure_openai_config.key + ) + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION = ( + form_data.azure_openai_config.version + ) + request.app.state.config.RAG_EMBEDDING_BATCH_SIZE = ( form_data.embedding_batch_size ) @@ -304,14 +331,27 @@ async def update_embedding_config( ( request.app.state.config.RAG_OPENAI_API_BASE_URL if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" - else request.app.state.config.RAG_OLLAMA_BASE_URL + else ( + request.app.state.config.RAG_OLLAMA_BASE_URL + if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + else request.app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) ), ( request.app.state.config.RAG_OPENAI_API_KEY if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" - else request.app.state.config.RAG_OLLAMA_API_KEY + else ( + request.app.state.config.RAG_OLLAMA_API_KEY + if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + else request.app.state.config.RAG_AZURE_OPENAI_API_KEY + ) ), request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION + if request.app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" + else None + ), ) return { @@ -327,6 +367,11 @@ async def update_embedding_config( "url": request.app.state.config.RAG_OLLAMA_BASE_URL, "key": request.app.state.config.RAG_OLLAMA_API_KEY, }, + "azure_openai_config": { + "url": request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, + "key": request.app.state.config.RAG_AZURE_OPENAI_API_KEY, + "version": request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, + }, } except Exception as e: log.exception(f"Problem updating embedding model: {e}") @@ -353,6 +398,15 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): # Content extraction settings "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, + "DATALAB_MARKER_API_KEY": request.app.state.config.DATALAB_MARKER_API_KEY, + "DATALAB_MARKER_LANGS": request.app.state.config.DATALAB_MARKER_LANGS, + "DATALAB_MARKER_SKIP_CACHE": request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + "DATALAB_MARKER_FORCE_OCR": request.app.state.config.DATALAB_MARKER_FORCE_OCR, + "DATALAB_MARKER_PAGINATE": request.app.state.config.DATALAB_MARKER_PAGINATE, + "DATALAB_MARKER_STRIP_EXISTING_OCR": request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION": request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + "DATALAB_MARKER_USE_LLM": request.app.state.config.DATALAB_MARKER_USE_LLM, + "DATALAB_MARKER_OUTPUT_FORMAT": request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, "EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, "EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, @@ -500,6 +554,15 @@ class ConfigForm(BaseModel): # Content extraction settings CONTENT_EXTRACTION_ENGINE: Optional[str] = None PDF_EXTRACT_IMAGES: Optional[bool] = None + DATALAB_MARKER_API_KEY: Optional[str] = None + DATALAB_MARKER_LANGS: Optional[str] = None + DATALAB_MARKER_SKIP_CACHE: Optional[bool] = None + DATALAB_MARKER_FORCE_OCR: Optional[bool] = None + DATALAB_MARKER_PAGINATE: Optional[bool] = None + DATALAB_MARKER_STRIP_EXISTING_OCR: Optional[bool] = None + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION: Optional[bool] = None + DATALAB_MARKER_USE_LLM: Optional[bool] = None + DATALAB_MARKER_OUTPUT_FORMAT: Optional[str] = None EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None @@ -599,6 +662,51 @@ async def update_rag_config( if form_data.PDF_EXTRACT_IMAGES is not None else request.app.state.config.PDF_EXTRACT_IMAGES ) + request.app.state.config.DATALAB_MARKER_API_KEY = ( + form_data.DATALAB_MARKER_API_KEY + if form_data.DATALAB_MARKER_API_KEY is not None + else request.app.state.config.DATALAB_MARKER_API_KEY + ) + request.app.state.config.DATALAB_MARKER_LANGS = ( + form_data.DATALAB_MARKER_LANGS + if form_data.DATALAB_MARKER_LANGS is not None + else request.app.state.config.DATALAB_MARKER_LANGS + ) + request.app.state.config.DATALAB_MARKER_SKIP_CACHE = ( + form_data.DATALAB_MARKER_SKIP_CACHE + if form_data.DATALAB_MARKER_SKIP_CACHE is not None + else request.app.state.config.DATALAB_MARKER_SKIP_CACHE + ) + request.app.state.config.DATALAB_MARKER_FORCE_OCR = ( + form_data.DATALAB_MARKER_FORCE_OCR + if form_data.DATALAB_MARKER_FORCE_OCR is not None + else request.app.state.config.DATALAB_MARKER_FORCE_OCR + ) + request.app.state.config.DATALAB_MARKER_PAGINATE = ( + form_data.DATALAB_MARKER_PAGINATE + if form_data.DATALAB_MARKER_PAGINATE is not None + else request.app.state.config.DATALAB_MARKER_PAGINATE + ) + request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = ( + form_data.DATALAB_MARKER_STRIP_EXISTING_OCR + if form_data.DATALAB_MARKER_STRIP_EXISTING_OCR is not None + else request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR + ) + request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = ( + form_data.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION + if form_data.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION is not None + else request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION + ) + request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = ( + form_data.DATALAB_MARKER_OUTPUT_FORMAT + if form_data.DATALAB_MARKER_OUTPUT_FORMAT is not None + else request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT + ) + request.app.state.config.DATALAB_MARKER_USE_LLM = ( + form_data.DATALAB_MARKER_USE_LLM + if form_data.DATALAB_MARKER_USE_LLM is not None + else request.app.state.config.DATALAB_MARKER_USE_LLM + ) request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = ( form_data.EXTERNAL_DOCUMENT_LOADER_URL if form_data.EXTERNAL_DOCUMENT_LOADER_URL is not None @@ -853,6 +961,15 @@ async def update_rag_config( # Content extraction settings "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, + "DATALAB_MARKER_API_KEY": request.app.state.config.DATALAB_MARKER_API_KEY, + "DATALAB_MARKER_LANGS": request.app.state.config.DATALAB_MARKER_LANGS, + "DATALAB_MARKER_SKIP_CACHE": request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + "DATALAB_MARKER_FORCE_OCR": request.app.state.config.DATALAB_MARKER_FORCE_OCR, + "DATALAB_MARKER_PAGINATE": request.app.state.config.DATALAB_MARKER_PAGINATE, + "DATALAB_MARKER_STRIP_EXISTING_OCR": request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION": request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + "DATALAB_MARKER_USE_LLM": request.app.state.config.DATALAB_MARKER_USE_LLM, + "DATALAB_MARKER_OUTPUT_FORMAT": request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, "EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, "EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, @@ -1057,14 +1174,27 @@ def save_docs_to_vector_db( ( request.app.state.config.RAG_OPENAI_API_BASE_URL if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" - else request.app.state.config.RAG_OLLAMA_BASE_URL + else ( + request.app.state.config.RAG_OLLAMA_BASE_URL + if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + else request.app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) ), ( request.app.state.config.RAG_OPENAI_API_KEY if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" - else request.app.state.config.RAG_OLLAMA_API_KEY + else ( + request.app.state.config.RAG_OLLAMA_API_KEY + if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + else request.app.state.config.RAG_AZURE_OPENAI_API_KEY + ) ), request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION + if request.app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" + else None + ), ) embeddings = embedding_function( @@ -1178,6 +1308,15 @@ def process_file( file_path = Storage.get_file(file_path) loader = Loader( engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE, + DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY, + DATALAB_MARKER_LANGS=request.app.state.config.DATALAB_MARKER_LANGS, + DATALAB_MARKER_SKIP_CACHE=request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_FORCE_OCR=request.app.state.config.DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_PAGINATE=request.app.state.config.DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_STRIP_EXISTING_OCR=request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION=request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_USE_LLM=request.app.state.config.DATALAB_MARKER_USE_LLM, + DATALAB_MARKER_OUTPUT_FORMAT=request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, EXTERNAL_DOCUMENT_LOADER_URL=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, EXTERNAL_DOCUMENT_LOADER_API_KEY=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL, @@ -1835,6 +1974,7 @@ class QueryCollectionsForm(BaseModel): k_reranker: Optional[int] = None r: Optional[float] = None hybrid: Optional[bool] = None + hybrid_bm25_weight: Optional[float] = None @router.post("/query/collection") diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index bd1ce8f625..f726368eba 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -2,6 +2,9 @@ import logging from pathlib import Path from typing import Optional import time +import re +import aiohttp +from pydantic import BaseModel, HttpUrl from open_webui.models.tools import ( ToolForm, @@ -21,6 +24,7 @@ from open_webui.env import SRC_LOG_LEVELS from open_webui.utils.tools import get_tool_servers_data + log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) @@ -95,6 +99,81 @@ async def get_tool_list(user=Depends(get_verified_user)): return tools +############################ +# LoadFunctionFromLink +############################ + + +class LoadUrlForm(BaseModel): + url: HttpUrl + + +def github_url_to_raw_url(url: str) -> str: + # Handle 'tree' (folder) URLs (add main.py at the end) + m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url) + if m1: + org, repo, branch, path = m1.groups() + return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py" + + # Handle 'blob' (file) URLs + m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url) + if m2: + org, repo, branch, path = m2.groups() + return ( + f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}" + ) + + # No match; return as-is + return url + + +@router.post("/load/url", response_model=Optional[dict]) +async def load_tool_from_url( + request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user) +): + # NOTE: This is NOT a SSRF vulnerability: + # This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use, + # and does NOT accept untrusted user input. Access is enforced by authentication. + + url = str(form_data.url) + if not url: + raise HTTPException(status_code=400, detail="Please enter a valid URL") + + url = github_url_to_raw_url(url) + url_parts = url.rstrip("/").split("/") + + file_name = url_parts[-1] + tool_name = ( + file_name[:-3] + if ( + file_name.endswith(".py") + and (not file_name.startswith(("main.py", "index.py", "__init__.py"))) + ) + else url_parts[-2] if len(url_parts) > 1 else "function" + ) + + try: + async with aiohttp.ClientSession() as session: + async with session.get( + url, headers={"Content-Type": "application/json"} + ) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, detail="Failed to fetch the tool" + ) + data = await resp.text() + if not data: + raise HTTPException( + status_code=400, detail="No data received from the URL" + ) + return { + "name": tool_name, + "content": data, + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error importing tool: {e}") + + ############################ # ExportTools ############################ diff --git a/backend/open_webui/static/assets/pdf-style.css b/backend/open_webui/static/assets/pdf-style.css index 7cb5b0cd24..8b4e8d2370 100644 --- a/backend/open_webui/static/assets/pdf-style.css +++ b/backend/open_webui/static/assets/pdf-style.css @@ -269,11 +269,6 @@ tbody + tbody { margin-bottom: 0; } -/* Add a rule to reset margin-bottom for

not followed by