diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d9865f82..22bf47ab3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ 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.5.17] - 2025-02-27 + +### Added + +- **🚀 Instant Document Upload with Bypass Embedding & Retrieval**: Admins can now enable "Bypass Embedding & Retrieval" in Admin Settings > Documents, significantly speeding up document uploads and ensuring full document context is retained without chunking. +- **🔎 "Stream" Hook for Real-Time Filtering**: The new "stream" hook allows dynamic real-time message filtering. Learn more in our documentation (https://docs.openwebui.com/features/plugin/functions/filter). +- **☁️ OneDrive Integration**: Early support for OneDrive storage integration has been introduced, expanding file import options. +- **📈 Enhanced Logging with Loguru**: Backend logging has been improved with Loguru, making debugging and issue tracking far more efficient. +- **⚙️ General Stability Enhancements**: Backend and frontend refactoring improves performance, ensuring a smoother and more reliable user experience. +- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages. + +### Fixed + +- **🔄 Reliable Model Imports from the Community Platform**: Resolved import failures, allowing seamless integration of community-shared models without errors. +- **📊 OpenAI Usage Statistics Restored**: Fixed an issue where OpenAI usage metrics were not displaying correctly, ensuring accurate tracking of usage data. +- **🗂️ Deduplication for Retrieved Documents**: Documents retrieved during searches are now intelligently deduplicated, meaning no more redundant results—helping to keep information concise and relevant. + +### Changed + +- **📝 "Full Context Mode" Renamed for Clarity**: The "Full Context Mode" toggle in Web Search settings is now labeled "Bypass Embedding & Retrieval" for consistency across the UI. + ## [0.5.16] - 2025-02-20 ### Fixed diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 5e0e4f0a17..15982f886f 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -9,7 +9,6 @@ from pathlib import Path from typing import Generic, Optional, TypeVar from urllib.parse import urlparse -import chromadb import requests from pydantic import BaseModel from sqlalchemy import JSON, Column, DateTime, Integer, func @@ -44,7 +43,7 @@ logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) # Function to run the alembic migrations def run_migrations(): - print("Running migrations") + log.info("Running migrations") try: from alembic import command from alembic.config import Config @@ -57,7 +56,7 @@ def run_migrations(): command.upgrade(alembic_cfg, "head") except Exception as e: - print(f"Error: {e}") + log.exception(f"Error running migrations: {e}") run_migrations() @@ -678,6 +677,10 @@ S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None) S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None) S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None) S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None) +S3_USE_ACCELERATE_ENDPOINT = ( + os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "False").lower() == "true" +) +S3_ADDRESSING_STYLE = os.environ.get("S3_ADDRESSING_STYLE", None) GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None) GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get( @@ -1094,7 +1097,7 @@ try: banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]")) banners = [BannerModel(**banner) for banner in banners] except Exception as e: - print(f"Error loading WEBUI_BANNERS: {e}") + log.exception(f"Error loading WEBUI_BANNERS: {e}") banners = [] WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners) @@ -1497,22 +1500,27 @@ Ensure that the tools are effectively utilized to achieve the highest-quality an VECTOR_DB = os.environ.get("VECTOR_DB", "chroma") # Chroma -CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" -CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT) -CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE) -CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "") -CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000")) -CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "") -CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "") -# Comma-separated list of header=value pairs -CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "") -if CHROMA_HTTP_HEADERS: - CHROMA_HTTP_HEADERS = dict( - [pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")] +if VECTOR_DB == "chroma": + import chromadb + + CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" + CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT) + CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE) + CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "") + CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000")) + CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "") + CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get( + "CHROMA_CLIENT_AUTH_CREDENTIALS", "" ) -else: - CHROMA_HTTP_HEADERS = None -CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true" + # Comma-separated list of header=value pairs + CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "") + if CHROMA_HTTP_HEADERS: + CHROMA_HTTP_HEADERS = dict( + [pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")] + ) + else: + CHROMA_HTTP_HEADERS = None + CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true" # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2) # Milvus @@ -1566,6 +1574,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig( os.environ.get("GOOGLE_DRIVE_API_KEY", ""), ) +ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig( + "ENABLE_ONEDRIVE_INTEGRATION", + "onedrive.enable", + os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true", +) + +ONEDRIVE_CLIENT_ID = PersistentConfig( + "ONEDRIVE_CLIENT_ID", + "onedrive.client_id", + os.environ.get("ONEDRIVE_CLIENT_ID", ""), +) + # RAG Content Extraction CONTENT_EXTRACTION_ENGINE = PersistentConfig( "CONTENT_EXTRACTION_ENGINE", @@ -1579,6 +1599,26 @@ TIKA_SERVER_URL = PersistentConfig( os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment ) +DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig( + "DOCUMENT_INTELLIGENCE_ENDPOINT", + "rag.document_intelligence_endpoint", + os.getenv("DOCUMENT_INTELLIGENCE_ENDPOINT", ""), +) + +DOCUMENT_INTELLIGENCE_KEY = PersistentConfig( + "DOCUMENT_INTELLIGENCE_KEY", + "rag.document_intelligence_key", + os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""), +) + + +BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig( + "BYPASS_EMBEDDING_AND_RETRIEVAL", + "rag.bypass_embedding_and_retrieval", + os.environ.get("BYPASS_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true", +) + + RAG_TOP_K = PersistentConfig( "RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3")) ) @@ -1795,10 +1835,10 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig( os.getenv("RAG_WEB_SEARCH_ENGINE", ""), ) -RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig( - "RAG_WEB_SEARCH_FULL_CONTEXT", - "rag.web.search.full_context", - os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true", +BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig( + "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", + "rag.web.search.bypass_embedding_and_retrieval", + os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true", ) # You can provide a list of your own websites to filter after performing a web search. diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 96e288d777..ba546a2eb5 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -419,3 +419,25 @@ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true" if OFFLINE_MODE: os.environ["HF_HUB_OFFLINE"] = "1" + +#################################### +# AUDIT LOGGING +#################################### +ENABLE_AUDIT_LOGS = os.getenv("ENABLE_AUDIT_LOGS", "false").lower() == "true" +# Where to store log file +AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log" +# Maximum size of a file before rotating into a new log file +AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB") +# METADATA | REQUEST | REQUEST_RESPONSE +AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "REQUEST_RESPONSE").upper() +try: + MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048) +except ValueError: + MAX_BODY_LOG_SIZE = 2048 + +# Comma separated list for urls to exclude from audit +AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split( + "," +) +AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS] +AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS] diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index 274be56ec0..2f94f701e9 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -2,6 +2,7 @@ import logging import sys import inspect import json +import asyncio from pydantic import BaseModel from typing import AsyncGenerator, Generator, Iterator @@ -76,11 +77,13 @@ async def get_function_models(request): if hasattr(function_module, "pipes"): sub_pipes = [] - # Check if pipes is a function or a list - + # Handle pipes being a list, sync function, or async function try: if callable(function_module.pipes): - sub_pipes = function_module.pipes() + if asyncio.iscoroutinefunction(function_module.pipes): + sub_pipes = await function_module.pipes() + else: + sub_pipes = function_module.pipes() else: sub_pipes = function_module.pipes except Exception as e: diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 9129bb4051..1e6f970c80 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -45,6 +45,9 @@ from starlette.middleware.sessions import SessionMiddleware from starlette.responses import Response, StreamingResponse +from open_webui.utils import logger +from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware +from open_webui.utils.logger import start_logger from open_webui.socket.main import ( app as socket_app, periodic_usage_pool_cleanup, @@ -95,6 +98,7 @@ from open_webui.config import ( OLLAMA_API_CONFIGS, # OpenAI ENABLE_OPENAI_API, + ONEDRIVE_CLIENT_ID, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, OPENAI_API_CONFIGS, @@ -161,6 +165,7 @@ from open_webui.config import ( RAG_TEMPLATE, DEFAULT_RAG_TEMPLATE, RAG_FULL_CONTEXT, + BYPASS_EMBEDDING_AND_RETRIEVAL, RAG_EMBEDDING_MODEL, RAG_EMBEDDING_MODEL_AUTO_UPDATE, RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, @@ -180,6 +185,8 @@ from open_webui.config import ( CHUNK_SIZE, CONTENT_EXTRACTION_ENGINE, TIKA_SERVER_URL, + DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY, RAG_TOP_K, RAG_TEXT_SPLITTER, TIKTOKEN_ENCODING_NAME, @@ -188,7 +195,7 @@ from open_webui.config import ( YOUTUBE_LOADER_PROXY_URL, # Retrieval (Web Search) RAG_WEB_SEARCH_ENGINE, - RAG_WEB_SEARCH_FULL_CONTEXT, + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, RAG_WEB_SEARCH_RESULT_COUNT, RAG_WEB_SEARCH_CONCURRENT_REQUESTS, RAG_WEB_SEARCH_TRUST_ENV, @@ -215,11 +222,13 @@ from open_webui.config import ( GOOGLE_PSE_ENGINE_ID, GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_API_KEY, + ONEDRIVE_CLIENT_ID, ENABLE_RAG_HYBRID_SEARCH, ENABLE_RAG_LOCAL_WEB_FETCH, ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, ENABLE_RAG_WEB_SEARCH, ENABLE_GOOGLE_DRIVE_INTEGRATION, + ENABLE_ONEDRIVE_INTEGRATION, UPLOAD_DIR, # WebUI WEBUI_AUTH, @@ -298,8 +307,11 @@ from open_webui.config import ( reset_config, ) from open_webui.env import ( + AUDIT_EXCLUDED_PATHS, + AUDIT_LOG_LEVEL, CHANGELOG, GLOBAL_LOG_LEVEL, + MAX_BODY_LOG_SIZE, SAFE_MODE, SRC_LOG_LEVELS, VERSION, @@ -384,6 +396,7 @@ https://github.com/open-webui/open-webui @asynccontextmanager async def lifespan(app: FastAPI): + start_logger() if RESET_CONFIG_ON_START: reset_config() @@ -526,6 +539,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 @@ -533,6 +547,8 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = ( app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL +app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT +app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME @@ -560,10 +576,13 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE -app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT +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 app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID @@ -879,6 +898,19 @@ app.include_router( app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) +try: + audit_level = AuditLevel(AUDIT_LOG_LEVEL) +except ValueError as e: + logger.error(f"Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}") + audit_level = AuditLevel.NONE + +if audit_level != AuditLevel.NONE: + app.add_middleware( + AuditLoggingMiddleware, + audit_level=audit_level, + excluded_paths=AUDIT_EXCLUDED_PATHS, + max_body_size=MAX_BODY_LOG_SIZE, + ) ################################## # # Chat Endpoints @@ -1146,6 +1178,7 @@ async def get_app_config(request: Request): "enable_admin_export": ENABLE_ADMIN_EXPORT, "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, } if user is not None else {} @@ -1177,6 +1210,7 @@ async def get_app_config(request: Request): "client_id": GOOGLE_DRIVE_CLIENT_ID.value, "api_key": GOOGLE_DRIVE_API_KEY.value, }, + "onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value}, } if user is not None else {} diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 9e0a5865e9..a222d221c0 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -1,3 +1,4 @@ +import logging import json import time import uuid @@ -5,7 +6,7 @@ from typing import Optional from open_webui.internal.db import Base, get_db from open_webui.models.tags import TagModel, Tag, Tags - +from open_webui.env import SRC_LOG_LEVELS from pydantic import BaseModel, ConfigDict from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON @@ -16,6 +17,9 @@ from sqlalchemy.sql import exists # Chat DB Schema #################### +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + class Chat(Base): __tablename__ = "chat" @@ -670,7 +674,7 @@ class ChatTable: # Perform pagination at the SQL level all_chats = query.offset(skip).limit(limit).all() - print(len(all_chats)) + log.info(f"The number of chats: {len(all_chats)}") # Validate and return chats return [ChatModel.model_validate(chat) for chat in all_chats] @@ -731,7 +735,7 @@ class ChatTable: query = db.query(Chat).filter_by(user_id=user_id) tag_id = tag_name.replace(" ", "_").lower() - print(db.bind.dialect.name) + log.info(f"DB dialect name: {db.bind.dialect.name}") if db.bind.dialect.name == "sqlite": # SQLite JSON1 querying for tags within the meta JSON field query = query.filter( @@ -752,7 +756,7 @@ class ChatTable: ) all_chats = query.all() - print("all_chats", all_chats) + log.debug(f"all_chats: {all_chats}") return [ChatModel.model_validate(chat) for chat in all_chats] def add_chat_tag_by_id_and_user_id_and_tag_name( @@ -810,7 +814,7 @@ class ChatTable: count = query.count() # Debugging output for inspection - print(f"Count of chats for tag '{tag_name}':", count) + log.info(f"Count of chats for tag '{tag_name}': {count}") return count diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 7ff5c45408..215e36aa24 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -118,7 +118,7 @@ class FeedbackTable: else: return None except Exception as e: - print(e) + log.exception(f"Error creating a new feedback: {e}") return None def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]: diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index 91dea54443..6f1511cd13 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -119,7 +119,7 @@ class FilesTable: else: return None except Exception as e: - print(f"Error creating tool: {e}") + log.exception(f"Error inserting a new file: {e}") return None def get_file_by_id(self, id: str) -> Optional[FileModel]: diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index 040774196b..19739bc5f5 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -82,7 +82,7 @@ class FolderTable: else: return None except Exception as e: - print(e) + log.exception(f"Error inserting a new folder: {e}") return None def get_folder_by_id_and_user_id( diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index 6c6aed8623..8cbfc5de7d 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -105,7 +105,7 @@ class FunctionsTable: else: return None except Exception as e: - print(f"Error creating tool: {e}") + log.exception(f"Error creating a new function: {e}") return None def get_function_by_id(self, id: str) -> Optional[FunctionModel]: @@ -170,7 +170,7 @@ class FunctionsTable: function = db.get(Function, id) return function.valves if function.valves else {} except Exception as e: - print(f"An error occurred: {e}") + log.exception(f"Error getting function valves by id {id}: {e}") return None def update_function_valves_by_id( @@ -202,7 +202,9 @@ class FunctionsTable: return user_settings["functions"]["valves"].get(id, {}) except Exception as e: - print(f"An error occurred: {e}") + log.exception( + f"Error getting user values by id {id} and user id {user_id}: {e}" + ) return None def update_user_valves_by_id_and_user_id( @@ -225,7 +227,9 @@ class FunctionsTable: return user_settings["functions"]["valves"][id] except Exception as e: - print(f"An error occurred: {e}") + log.exception( + f"Error updating user valves by id {id} and user_id {user_id}: {e}" + ) return None def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]: diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py old mode 100644 new mode 100755 index f2f59d7c49..7df8d8656b --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -166,7 +166,7 @@ class ModelsTable: else: return None except Exception as e: - print(e) + log.exception(f"Failed to insert a new model: {e}") return None def get_all_models(self) -> list[ModelModel]: @@ -246,8 +246,7 @@ class ModelsTable: db.refresh(model) return ModelModel.model_validate(model) except Exception as e: - print(e) - + log.exception(f"Failed to update the model by id {id}: {e}") return None def delete_model_by_id(self, id: str) -> bool: diff --git a/backend/open_webui/models/tags.py b/backend/open_webui/models/tags.py index 3e812db95d..279dc624d5 100644 --- a/backend/open_webui/models/tags.py +++ b/backend/open_webui/models/tags.py @@ -61,7 +61,7 @@ class TagTable: else: return None except Exception as e: - print(e) + log.exception(f"Error inserting a new tag: {e}") return None def get_tag_by_name_and_user_id( diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index a5f13ebb71..68a83ea42c 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -131,7 +131,7 @@ class ToolsTable: else: return None except Exception as e: - print(f"Error creating tool: {e}") + log.exception(f"Error creating a new tool: {e}") return None def get_tool_by_id(self, id: str) -> Optional[ToolModel]: @@ -175,7 +175,7 @@ class ToolsTable: tool = db.get(Tool, id) return tool.valves if tool.valves else {} except Exception as e: - print(f"An error occurred: {e}") + log.exception(f"Error getting tool valves by id {id}: {e}") return None def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]: @@ -204,7 +204,9 @@ class ToolsTable: return user_settings["tools"]["valves"].get(id, {}) except Exception as e: - print(f"An error occurred: {e}") + log.exception( + f"Error getting user values by id {id} and user_id {user_id}: {e}" + ) return None def update_user_valves_by_id_and_user_id( @@ -227,7 +229,9 @@ class ToolsTable: return user_settings["tools"]["valves"][id] except Exception as e: - print(f"An error occurred: {e}") + log.exception( + f"Error updating user valves by id {id} and user_id {user_id}: {e}" + ) return None def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]: diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index a9372f65a6..7fa24ced37 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -4,6 +4,7 @@ import ftfy import sys from langchain_community.document_loaders import ( + AzureAIDocumentIntelligenceLoader, BSHTMLLoader, CSVLoader, Docx2txtLoader, @@ -76,6 +77,7 @@ known_source_ext = [ "jsx", "hs", "lhs", + "json", ] @@ -147,6 +149,27 @@ class Loader: file_path=file_path, mime_type=file_content_type, ) + elif ( + self.engine == "document_intelligence" + and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != "" + and self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != "" + and ( + file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"] + or file_content_type + in [ + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ] + ) + ): + loader = AzureAIDocumentIntelligenceLoader( + file_path=file_path, + api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"), + api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"), + ) else: if file_ext == "pdf": loader = PyPDFLoader( diff --git a/backend/open_webui/retrieval/models/colbert.py b/backend/open_webui/retrieval/models/colbert.py index ea3204cb8b..5b7499fd18 100644 --- a/backend/open_webui/retrieval/models/colbert.py +++ b/backend/open_webui/retrieval/models/colbert.py @@ -1,13 +1,19 @@ import os +import logging import torch import numpy as np from colbert.infra import ColBERTConfig from colbert.modeling.checkpoint import Checkpoint +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + class ColBERT: def __init__(self, name, **kwargs) -> None: - print("ColBERT: Loading model", name) + log.info("ColBERT: Loading model", name) self.device = "cuda" if torch.cuda.is_available() else "cpu" DOCKER = kwargs.get("env") == "docker" diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index f83d09d9c5..b6253e63cc 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 asyncio import requests +import hashlib from huggingface_hub import snapshot_download from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever @@ -17,6 +18,7 @@ from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.utils.misc import get_last_user_message, calculate_sha256_string from open_webui.models.users import UserModel +from open_webui.models.files import Files from open_webui.env import ( SRC_LOG_LEVELS, @@ -81,7 +83,7 @@ def query_doc( return result except Exception as e: - print(e) + log.exception(f"Error querying doc {collection_name} with limit {k}: {e}") raise e @@ -94,7 +96,7 @@ def get_doc(collection_name: str, user: UserModel = None): return result except Exception as e: - print(e) + log.exception(f"Error getting doc {collection_name}: {e}") raise e @@ -174,46 +176,41 @@ def merge_get_results(get_results: list[dict]) -> dict: def merge_and_sort_query_results( query_results: list[dict], k: int, reverse: bool = False -) -> list[dict]: +) -> dict: # Initialize lists to store combined data - combined_distances = [] - combined_documents = [] - combined_metadatas = [] + combined = [] + seen_hashes = set() # To store unique document hashes for data in query_results: - combined_distances.extend(data["distances"][0]) - combined_documents.extend(data["documents"][0]) - combined_metadatas.extend(data["metadatas"][0]) + distances = data["distances"][0] + documents = data["documents"][0] + metadatas = data["metadatas"][0] - # Create a list of tuples (distance, document, metadata) - combined = list(zip(combined_distances, combined_documents, combined_metadatas)) + for distance, document, metadata in zip(distances, documents, metadatas): + if isinstance(document, str): + doc_hash = hashlib.md5( + document.encode() + ).hexdigest() # Compute a hash for uniqueness + + if doc_hash not in seen_hashes: + seen_hashes.add(doc_hash) + combined.append((distance, document, metadata)) # Sort the list based on distances combined.sort(key=lambda x: x[0], reverse=reverse) - # We don't have anything :-( - if not combined: - sorted_distances = [] - sorted_documents = [] - sorted_metadatas = [] - else: - # Unzip the sorted list - sorted_distances, sorted_documents, sorted_metadatas = zip(*combined) + # Slice to keep only the top k elements + sorted_distances, sorted_documents, sorted_metadatas = ( + zip(*combined[:k]) if combined else ([], [], []) + ) - # Slicing the lists to include only k elements - sorted_distances = list(sorted_distances)[:k] - sorted_documents = list(sorted_documents)[:k] - sorted_metadatas = list(sorted_metadatas)[:k] - - # Create the output dictionary - result = { - "distances": [sorted_distances], - "documents": [sorted_documents], - "metadatas": [sorted_metadatas], + # Create and return the output dictionary + return { + "distances": [list(sorted_distances)], + "documents": [list(sorted_documents)], + "metadatas": [list(sorted_metadatas)], } - return result - def get_all_items_from_collections(collection_names: list[str]) -> dict: results = [] @@ -342,6 +339,7 @@ def get_embedding_function( def get_sources_from_files( + request, files, queries, embedding_function, @@ -359,19 +357,64 @@ def get_sources_from_files( relevant_contexts = [] for file in files: + + context = None if file.get("docs"): + # BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL context = { "documents": [[doc.get("content") for doc in file.get("docs")]], "metadatas": [[doc.get("metadata") for doc in file.get("docs")]], } elif file.get("context") == "full": + # Manual Full Mode Toggle context = { "documents": [[file.get("file").get("data", {}).get("content")]], "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]], } - else: - context = None + elif ( + file.get("type") != "web_search" + and request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL + ): + # BYPASS_EMBEDDING_AND_RETRIEVAL + if file.get("type") == "collection": + file_ids = file.get("data", {}).get("file_ids", []) + documents = [] + metadatas = [] + for file_id in file_ids: + file_object = Files.get_file_by_id(file_id) + + if file_object: + documents.append(file_object.data.get("content", "")) + metadatas.append( + { + "file_id": file_id, + "name": file_object.filename, + "source": file_object.filename, + } + ) + + context = { + "documents": [documents], + "metadatas": [metadatas], + } + + elif file.get("id"): + file_object = Files.get_file_by_id(file.get("id")) + if file_object: + context = { + "documents": [[file_object.data.get("content", "")]], + "metadatas": [ + [ + { + "file_id": file.get("id"), + "name": file_object.filename, + "source": file_object.filename, + } + ] + ], + } + else: collection_names = [] if file.get("type") == "collection": if file.get("legacy"): @@ -434,6 +477,7 @@ def get_sources_from_files( if context: if "data" in file: del file["data"] + relevant_contexts.append({**context, "file": file}) sources = [] @@ -530,7 +574,7 @@ def generate_openai_batch_embeddings( else: raise "Something went wrong :/" except Exception as e: - print(e) + log.exception(f"Error generating openai batch embeddings: {e}") return None @@ -564,7 +608,7 @@ def generate_ollama_batch_embeddings( else: raise "Something went wrong :/" except Exception as e: - print(e) + log.exception(f"Error generating ollama batch embeddings: {e}") return None diff --git a/backend/open_webui/retrieval/vector/dbs/chroma.py b/backend/open_webui/retrieval/vector/dbs/chroma.py old mode 100644 new mode 100755 index c40618fcc5..006ee20763 --- a/backend/open_webui/retrieval/vector/dbs/chroma.py +++ b/backend/open_webui/retrieval/vector/dbs/chroma.py @@ -1,4 +1,5 @@ import chromadb +import logging from chromadb import Settings from chromadb.utils.batch_utils import create_batches @@ -16,6 +17,10 @@ from open_webui.config import ( CHROMA_CLIENT_AUTH_PROVIDER, CHROMA_CLIENT_AUTH_CREDENTIALS, ) +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) class ChromaClient: @@ -102,8 +107,7 @@ class ChromaClient: } ) return None - except Exception as e: - print(e) + except: return None def get(self, collection_name: str) -> Optional[GetResult]: diff --git a/backend/open_webui/retrieval/vector/dbs/milvus.py b/backend/open_webui/retrieval/vector/dbs/milvus.py index 43c3f3d1a1..3fd5b1ccd3 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus.py @@ -1,7 +1,7 @@ from pymilvus import MilvusClient as Client from pymilvus import FieldSchema, DataType import json - +import logging from typing import Optional from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult @@ -10,6 +10,10 @@ from open_webui.config import ( MILVUS_DB, MILVUS_TOKEN, ) +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) class MilvusClient: @@ -168,7 +172,7 @@ class MilvusClient: try: # Loop until there are no more items to fetch or the desired limit is reached while remaining > 0: - print("remaining", remaining) + log.info(f"remaining: {remaining}") current_fetch = min( max_limit, remaining ) # Determine how many items to fetch in this iteration @@ -195,10 +199,12 @@ class MilvusClient: if results_count < current_fetch: break - print(all_results) + log.debug(all_results) return self._result_to_get_result([all_results]) except Exception as e: - print(e) + log.exception( + f"Error querying collection {collection_name} with limit {limit}: {e}" + ) return None def get(self, collection_name: str) -> Optional[GetResult]: diff --git a/backend/open_webui/retrieval/vector/dbs/pgvector.py b/backend/open_webui/retrieval/vector/dbs/pgvector.py index 341b3056fa..eab02232f4 100644 --- a/backend/open_webui/retrieval/vector/dbs/pgvector.py +++ b/backend/open_webui/retrieval/vector/dbs/pgvector.py @@ -1,4 +1,5 @@ from typing import Optional, List, Dict, Any +import logging from sqlalchemy import ( cast, column, @@ -24,9 +25,14 @@ from sqlalchemy.exc import NoSuchTableError from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH +from open_webui.env import SRC_LOG_LEVELS + VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH Base = declarative_base() +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + class DocumentChunk(Base): __tablename__ = "document_chunk" @@ -82,10 +88,10 @@ class PgvectorClient: ) ) self.session.commit() - print("Initialization complete.") + log.info("Initialization complete.") except Exception as e: self.session.rollback() - print(f"Error during initialization: {e}") + log.exception(f"Error during initialization: {e}") raise def check_vector_length(self) -> None: @@ -150,12 +156,12 @@ class PgvectorClient: new_items.append(new_chunk) self.session.bulk_save_objects(new_items) self.session.commit() - print( + log.info( f"Inserted {len(new_items)} items into collection '{collection_name}'." ) except Exception as e: self.session.rollback() - print(f"Error during insert: {e}") + log.exception(f"Error during insert: {e}") raise def upsert(self, collection_name: str, items: List[VectorItem]) -> None: @@ -184,10 +190,12 @@ class PgvectorClient: ) self.session.add(new_chunk) self.session.commit() - print(f"Upserted {len(items)} items into collection '{collection_name}'.") + log.info( + f"Upserted {len(items)} items into collection '{collection_name}'." + ) except Exception as e: self.session.rollback() - print(f"Error during upsert: {e}") + log.exception(f"Error during upsert: {e}") raise def search( @@ -278,7 +286,7 @@ class PgvectorClient: ids=ids, distances=distances, documents=documents, metadatas=metadatas ) except Exception as e: - print(f"Error during search: {e}") + log.exception(f"Error during search: {e}") return None def query( @@ -310,7 +318,7 @@ class PgvectorClient: metadatas=metadatas, ) except Exception as e: - print(f"Error during query: {e}") + log.exception(f"Error during query: {e}") return None def get( @@ -334,7 +342,7 @@ class PgvectorClient: return GetResult(ids=ids, documents=documents, metadatas=metadatas) except Exception as e: - print(f"Error during get: {e}") + log.exception(f"Error during get: {e}") return None def delete( @@ -356,22 +364,22 @@ class PgvectorClient: ) deleted = query.delete(synchronize_session=False) self.session.commit() - print(f"Deleted {deleted} items from collection '{collection_name}'.") + log.info(f"Deleted {deleted} items from collection '{collection_name}'.") except Exception as e: self.session.rollback() - print(f"Error during delete: {e}") + log.exception(f"Error during delete: {e}") raise def reset(self) -> None: try: deleted = self.session.query(DocumentChunk).delete() self.session.commit() - print( + log.info( f"Reset complete. Deleted {deleted} items from 'document_chunk' table." ) except Exception as e: self.session.rollback() - print(f"Error during reset: {e}") + log.exception(f"Error during reset: {e}") raise def close(self) -> None: @@ -387,9 +395,9 @@ class PgvectorClient: ) return exists except Exception as e: - print(f"Error checking collection existence: {e}") + log.exception(f"Error checking collection existence: {e}") return False def delete_collection(self, collection_name: str) -> None: self.delete(collection_name) - print(f"Collection '{collection_name}' deleted.") + log.info(f"Collection '{collection_name}' deleted.") diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant.py b/backend/open_webui/retrieval/vector/dbs/qdrant.py index f077ae45ac..28f0b37793 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant.py @@ -1,4 +1,5 @@ from typing import Optional +import logging from qdrant_client import QdrantClient as Qclient from qdrant_client.http.models import PointStruct @@ -6,9 +7,13 @@ from qdrant_client.models import models from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult from open_webui.config import QDRANT_URI, QDRANT_API_KEY +from open_webui.env import SRC_LOG_LEVELS NO_LIMIT = 999999999 +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + class QdrantClient: def __init__(self): @@ -49,7 +54,7 @@ class QdrantClient: ), ) - print(f"collection {collection_name_with_prefix} successfully created!") + log.info(f"collection {collection_name_with_prefix} successfully created!") def _create_collection_if_not_exists(self, collection_name, dimension): if not self.has_collection(collection_name=collection_name): @@ -120,7 +125,7 @@ class QdrantClient: ) return self._result_to_get_result(points.points) except Exception as e: - print(e) + log.exception(f"Error querying a collection '{collection_name}': {e}") return None def get(self, collection_name: str) -> Optional[GetResult]: diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index a970366d1e..c949e65a46 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -71,7 +71,7 @@ from pydub.utils import mediainfo def is_mp4_audio(file_path): """Check if the given file is an MP4 audio file.""" if not os.path.isfile(file_path): - print(f"File not found: {file_path}") + log.error(f"File not found: {file_path}") return False info = mediainfo(file_path) @@ -88,7 +88,7 @@ def convert_mp4_to_wav(file_path, output_path): """Convert MP4 audio file to WAV format.""" audio = AudioSegment.from_file(file_path, format="mp4") audio.export(output_path, format="wav") - print(f"Converted {file_path} to {output_path}") + log.info(f"Converted {file_path} to {output_path}") def set_faster_whisper_model(model: str, auto_update: bool = False): @@ -266,7 +266,6 @@ async def speech(request: Request, user=Depends(get_verified_user)): payload["model"] = request.app.state.config.TTS_MODEL try: - # print(payload) timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) async with aiohttp.ClientSession( timeout=timeout, trust_env=True @@ -468,7 +467,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): def transcribe(request: Request, file_path): - print("transcribe", file_path) + log.info(f"transcribe: {file_path}") filename = os.path.basename(file_path) file_dir = os.path.dirname(file_path) id = filename.split(".")[0] @@ -680,7 +679,22 @@ def transcription( def get_available_models(request: Request) -> list[dict]: available_models = [] if request.app.state.config.TTS_ENGINE == "openai": - available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}] + # Use custom endpoint if not using the official OpenAI API URL + if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith( + "https://api.openai.com" + ): + try: + response = requests.get( + f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models" + ) + response.raise_for_status() + data = response.json() + available_models = data.get("models", []) + except Exception as e: + log.error(f"Error fetching models from custom endpoint: {str(e)}") + available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}] + else: + available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}] elif request.app.state.config.TTS_ENGINE == "elevenlabs": try: response = requests.get( @@ -711,14 +725,37 @@ def get_available_voices(request) -> dict: """Returns {voice_id: voice_name} dict""" available_voices = {} if request.app.state.config.TTS_ENGINE == "openai": - available_voices = { - "alloy": "alloy", - "echo": "echo", - "fable": "fable", - "onyx": "onyx", - "nova": "nova", - "shimmer": "shimmer", - } + # Use custom endpoint if not using the official OpenAI API URL + if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith( + "https://api.openai.com" + ): + try: + response = requests.get( + f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices" + ) + response.raise_for_status() + data = response.json() + voices_list = data.get("voices", []) + available_voices = {voice["id"]: voice["name"] for voice in voices_list} + except Exception as e: + log.error(f"Error fetching voices from custom endpoint: {str(e)}") + available_voices = { + "alloy": "alloy", + "echo": "echo", + "fable": "fable", + "onyx": "onyx", + "nova": "nova", + "shimmer": "shimmer", + } + else: + available_voices = { + "alloy": "alloy", + "echo": "echo", + "fable": "fable", + "onyx": "onyx", + "nova": "nova", + "shimmer": "shimmer", + } elif request.app.state.config.TTS_ENGINE == "elevenlabs": try: available_voices = get_elevenlabs_voices( diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 3fa2ffe2e9..f01b5bd747 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -31,10 +31,7 @@ from open_webui.env import ( ) from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import RedirectResponse, Response -from open_webui.config import ( - OPENID_PROVIDER_URL, - ENABLE_OAUTH_SIGNUP, -) +from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP from pydantic import BaseModel from open_webui.utils.misc import parse_duration, validate_email_format from open_webui.utils.auth import ( @@ -51,8 +48,10 @@ from open_webui.utils.access_control import get_permissions from typing import Optional, List from ssl import CERT_REQUIRED, PROTOCOL_TLS -from ldap3 import Server, Connection, NONE, Tls -from ldap3.utils.conv import escape_filter_chars + +if ENABLE_LDAP.value: + from ldap3 import Server, Connection, NONE, Tls + from ldap3.utils.conv import escape_filter_chars router = APIRouter() @@ -252,14 +251,6 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): if not user: try: user_count = Users.get_num_users() - if ( - request.app.state.USER_COUNT - and user_count >= request.app.state.USER_COUNT - ): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) role = ( "admin" @@ -439,11 +430,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm): ) user_count = Users.get_num_users() - if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT: - raise HTTPException( - status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED - ) - if not validate_email_format(form_data.email.lower()): raise HTTPException( status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT @@ -613,7 +599,7 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)): admin_email = request.app.state.config.ADMIN_EMAIL admin_name = None - print(admin_email, admin_name) + log.info(f"Admin details - Email: {admin_email}, Name: {admin_name}") if admin_email: admin = Users.get_user_by_email(admin_email) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 504baa60dc..95b7f6461a 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -16,6 +16,7 @@ from open_webui.models.files import ( Files, ) from open_webui.routers.retrieval import ProcessFileForm, process_file +from open_webui.routers.audio import transcribe from open_webui.storage.provider import Storage from open_webui.utils.auth import get_admin_user, get_verified_user from pydantic import BaseModel @@ -67,7 +68,22 @@ def upload_file( ) try: - process_file(request, ProcessFileForm(file_id=id), user=user) + if file.content_type in [ + "audio/mpeg", + "audio/wav", + "audio/ogg", + "audio/x-m4a", + ]: + file_path = Storage.get_file(file_path) + result = transcribe(request, file_path) + process_file( + request, + ProcessFileForm(file_id=id, content=result.get("text", "")), + user=user, + ) + else: + process_file(request, ProcessFileForm(file_id=id), user=user) + file_item = Files.get_file_by_id(id=id) except Exception as e: log.exception(e) @@ -273,7 +289,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)): # Check if the file already exists in the cache if file_path.is_file(): - print(f"file_path: {file_path}") + log.info(f"file_path: {file_path}") return FileResponse(file_path) else: raise HTTPException( diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 7f3305f25a..ac2db9322a 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -1,4 +1,5 @@ import os +import logging from pathlib import Path from typing import Optional @@ -13,6 +14,11 @@ 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.auth import get_admin_user, get_verified_user +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) + router = APIRouter() @@ -79,7 +85,7 @@ async def create_new_function( detail=ERROR_MESSAGES.DEFAULT("Error creating function"), ) except Exception as e: - print(e) + log.exception(f"Failed to create a new function: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -183,7 +189,7 @@ async def update_function_by_id( FUNCTIONS[id] = function_module updated = {**form_data.model_dump(exclude={"id"}), "type": function_type} - print(updated) + log.debug(updated) function = Functions.update_function_by_id(id, updated) @@ -299,7 +305,7 @@ async def update_function_valves_by_id( Functions.update_function_valves_by_id(id, valves.model_dump()) return valves.model_dump() except Exception as e: - print(e) + log.exception(f"Error updating function values by id {id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -388,7 +394,7 @@ async def update_function_user_valves_by_id( ) return user_valves.model_dump() except Exception as e: - print(e) + log.exception(f"Error updating function user valves by id {id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py old mode 100644 new mode 100755 index 5b5130f71d..ae822c0d00 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -1,7 +1,7 @@ import os from pathlib import Path from typing import Optional - +import logging from open_webui.models.users import Users from open_webui.models.groups import ( @@ -14,7 +14,13 @@ from open_webui.models.groups import ( from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES from fastapi import APIRouter, Depends, HTTPException, Request, status + from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.env import SRC_LOG_LEVELS + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) router = APIRouter() @@ -37,7 +43,7 @@ async def get_groups(user=Depends(get_verified_user)): @router.post("/create", response_model=Optional[GroupResponse]) -async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)): +async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)): try: group = Groups.insert_new_group(user.id, form_data) if group: @@ -48,7 +54,7 @@ async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user) detail=ERROR_MESSAGES.DEFAULT("Error creating group"), ) except Exception as e: - print(e) + log.exception(f"Error creating a new group: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -94,7 +100,7 @@ async def update_group_by_id( detail=ERROR_MESSAGES.DEFAULT("Error updating group"), ) except Exception as e: - print(e) + log.exception(f"Error updating group {id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -118,7 +124,7 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user)): detail=ERROR_MESSAGES.DEFAULT("Error deleting group"), ) except Exception as e: - print(e) + log.exception(f"Error deleting group {id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 3288ec6d84..131fa2df4d 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -144,6 +144,8 @@ async def update_config( request.app.state.config.COMFYUI_BASE_URL = ( form_data.comfyui.COMFYUI_BASE_URL.strip("/") ) + request.app.state.config.COMFYUI_API_KEY = form_data.comfyui.COMFYUI_API_KEY + request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW request.app.state.config.COMFYUI_WORKFLOW_NODES = ( form_data.comfyui.COMFYUI_WORKFLOW_NODES @@ -203,9 +205,17 @@ async def verify_url(request: Request, user=Depends(get_admin_user)): request.app.state.config.ENABLE_IMAGE_GENERATION = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": + + headers = None + if request.app.state.config.COMFYUI_API_KEY: + headers = { + "Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}" + } + try: r = requests.get( - url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info" + url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info", + headers=headers, ) r.raise_for_status() return True @@ -351,7 +361,7 @@ def get_models(request: Request, user=Depends(get_verified_user)): if model_node_id: model_list_key = None - print(workflow[model_node_id]["class_type"]) + log.info(workflow[model_node_id]["class_type"]) for key in info[workflow[model_node_id]["class_type"]]["input"][ "required" ]: diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 0ba6191a2a..1969045505 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -614,7 +614,7 @@ def add_files_to_knowledge_batch( ) # Get files content - print(f"files/batch/add - {len(form_data)} files") + log.info(f"files/batch/add - {len(form_data)} files") files: List[FileModel] = [] for form in form_data: file = Files.get_file_by_id(form.file_id) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 1ef913df47..dff2461eac 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -98,9 +98,15 @@ def openai_o1_o3_handler(payload): payload["max_completion_tokens"] = payload["max_tokens"] del payload["max_tokens"] - # Fix: O1 does not support the "system" parameter, Modify "system" to "user" + # Fix: o1 and o3 do not support the "system" role directly. + # For older models like "o1-mini" or "o1-preview", use role "user". + # For newer o1/o3 models, replace "system" with "developer". if payload["messages"][0]["role"] == "system": - payload["messages"][0]["role"] = "user" + model_lower = payload["model"].lower() + if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"): + payload["messages"][0]["role"] = "user" + else: + payload["messages"][0]["role"] = "developer" return payload @@ -804,7 +810,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): if r is not None: try: res = await r.json() - print(res) + log.error(res) if "error" in res: detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}" except Exception: diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py index ad280b65c1..599208e43d 100644 --- a/backend/open_webui/routers/pipelines.py +++ b/backend/open_webui/routers/pipelines.py @@ -101,7 +101,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models): if "detail" in res: raise Exception(response.status, res["detail"]) except Exception as e: - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") return payload @@ -153,7 +153,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models): except Exception: pass except Exception as e: - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") return payload @@ -169,7 +169,7 @@ router = APIRouter() @router.get("/list") async def get_pipelines_list(request: Request, user=Depends(get_admin_user)): - responses = await get_all_models_responses(request) + responses = await get_all_models_responses(request, user) log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}") urlIdxs = [ @@ -196,7 +196,7 @@ async def upload_pipeline( file: UploadFile = File(...), user=Depends(get_admin_user), ): - print("upload_pipeline", urlIdx, file.filename) + log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}") # Check if the uploaded file is a python file if not (file.filename and file.filename.endswith(".py")): raise HTTPException( @@ -231,7 +231,7 @@ async def upload_pipeline( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None status_code = status.HTTP_404_NOT_FOUND @@ -282,7 +282,7 @@ async def add_pipeline( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None if r is not None: @@ -327,7 +327,7 @@ async def delete_pipeline( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None if r is not None: @@ -361,7 +361,7 @@ async def get_pipelines( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None if r is not None: @@ -400,7 +400,7 @@ async def get_pipeline_valves( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None if r is not None: @@ -440,7 +440,7 @@ async def get_pipeline_valves_spec( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None if r is not None: @@ -482,7 +482,7 @@ async def update_pipeline_valves( return {**data} except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.exception(f"Connection error: {e}") detail = None diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index e69d2ce963..7dd324b80b 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -352,10 +352,16 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "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, "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, + "document_intelligence_config": { + "endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + "key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + }, }, "chunk": { "text_splitter": request.app.state.config.TEXT_SPLITTER, @@ -373,10 +379,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): }, "web": { "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, - "RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT, + "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, @@ -399,6 +406,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, "exa_api_key": request.app.state.config.EXA_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, }, @@ -411,9 +419,15 @@ class FileConfig(BaseModel): max_count: Optional[int] = None +class DocumentIntelligenceConfigForm(BaseModel): + endpoint: str + key: str + + class ContentExtractionConfig(BaseModel): engine: str = "" tika_server_url: Optional[str] = None + document_intelligence_config: Optional[DocumentIntelligenceConfigForm] = None class ChunkParamUpdateForm(BaseModel): @@ -460,13 +474,15 @@ class WebSearchConfig(BaseModel): class WebConfig(BaseModel): search: WebSearchConfig ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None - RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None class ConfigUpdateForm(BaseModel): RAG_FULL_CONTEXT: Optional[bool] = 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 @@ -490,24 +506,45 @@ async def update_rag_config( 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 text settings: {form_data.content_extraction}") + log.info( + f"Updating content extraction: {request.app.state.config.CONTENT_EXTRACTION_ENGINE} to {form_data.content_extraction.engine}" + ) request.app.state.config.CONTENT_EXTRACTION_ENGINE = ( form_data.content_extraction.engine ) request.app.state.config.TIKA_SERVER_URL = ( form_data.content_extraction.tika_server_url ) + if form_data.content_extraction.document_intelligence_config is not None: + request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = ( + form_data.content_extraction.document_intelligence_config.endpoint + ) + request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = ( + form_data.content_extraction.document_intelligence_config.key + ) if form_data.chunk is not None: request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter @@ -528,8 +565,8 @@ async def update_rag_config( request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine - request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = ( - form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT + 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 = ( @@ -597,6 +634,7 @@ async def update_rag_config( "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, @@ -604,6 +642,10 @@ async def update_rag_config( "content_extraction": { "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "tika_server_url": request.app.state.config.TIKA_SERVER_URL, + "document_intelligence_config": { + "endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + "key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + }, }, "chunk": { "text_splitter": request.app.state.config.TEXT_SPLITTER, @@ -617,7 +659,7 @@ async def update_rag_config( }, "web": { "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, - "RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT, + "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, @@ -880,7 +922,12 @@ def process_file( # Update the content in the file # Usage: /files/{file_id}/data/content/update - VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}") + try: + # /files/{file_id}/data/content/update + VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}") + except: + # Audio file upload pipeline + pass docs = [ Document( @@ -937,6 +984,8 @@ def process_file( engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE, TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL, PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, + DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, ) docs = loader.load( file.filename, file.meta.get("content_type"), file_path @@ -979,36 +1028,45 @@ def process_file( hash = calculate_sha256_string(text_content) Files.update_file_hash_by_id(file.id, hash) - try: - result = save_docs_to_vector_db( - request, - docs=docs, - collection_name=collection_name, - metadata={ - "file_id": file.id, - "name": file.filename, - "hash": hash, - }, - add=(True if form_data.collection_name else False), - user=user, - ) - - if result: - Files.update_file_metadata_by_id( - file.id, - { - "collection_name": collection_name, + if not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + try: + result = save_docs_to_vector_db( + request, + docs=docs, + collection_name=collection_name, + metadata={ + "file_id": file.id, + "name": file.filename, + "hash": hash, }, + add=(True if form_data.collection_name else False), + user=user, ) - return { - "status": True, - "collection_name": collection_name, - "filename": file.filename, - "content": text_content, - } - except Exception as e: - raise e + if result: + Files.update_file_metadata_by_id( + file.id, + { + "collection_name": collection_name, + }, + ) + + return { + "status": True, + "collection_name": collection_name, + "filename": file.filename, + "content": text_content, + } + except Exception as e: + raise e + else: + return { + "status": True, + "collection_name": None, + "filename": file.filename, + "content": text_content, + } + except Exception as e: log.exception(e) if "No pandoc was found" in str(e): @@ -1368,9 +1426,11 @@ async def process_web_search( ) docs = await loader.aload() - if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT: + if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: return { "status": True, + "collection_name": None, + "filenames": urls, "docs": [ { "content": doc.page_content, @@ -1378,7 +1438,6 @@ async def process_web_search( } for doc in docs ], - "filenames": urls, "loaded_count": len(docs), } else: @@ -1553,11 +1612,11 @@ def reset_upload_dir(user=Depends(get_admin_user)) -> bool: elif os.path.isdir(file_path): shutil.rmtree(file_path) # Remove the directory except Exception as e: - print(f"Failed to delete {file_path}. Reason: {e}") + log.exception(f"Failed to delete {file_path}. Reason: {e}") else: - print(f"The directory {folder} does not exist") + log.warning(f"The directory {folder} does not exist") except Exception as e: - print(f"Failed to process the directory {folder}. Reason: {e}") + log.exception(f"Failed to process the directory {folder}. Reason: {e}") return True diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 0328cefe04..b63c9732af 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -20,6 +20,10 @@ from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.constants import TASKS from open_webui.routers.pipelines import process_pipeline_inlet_filter +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) from open_webui.utils.task import get_task_model_id from open_webui.config import ( @@ -221,6 +225,12 @@ async def generate_title( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: @@ -290,6 +300,12 @@ async def generate_chat_tags( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: @@ -356,6 +372,12 @@ async def generate_image_prompt( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: @@ -433,6 +455,12 @@ async def generate_queries( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: @@ -514,6 +542,12 @@ async def generate_autocompletion( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: @@ -584,6 +618,12 @@ async def generate_emoji( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: @@ -644,6 +684,12 @@ async def generate_moa_response( }, } + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index d6a5c5532f..5e4109037d 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from typing import Optional @@ -15,6 +16,10 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.utils.tools import get_tools_specs from open_webui.utils.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 + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) router = APIRouter() @@ -111,7 +116,7 @@ async def create_new_tools( detail=ERROR_MESSAGES.DEFAULT("Error creating tools"), ) except Exception as e: - print(e) + log.exception(f"Failed to load the tool by id {form_data.id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(str(e)), @@ -193,7 +198,7 @@ async def update_tools_by_id( "specs": specs, } - print(updated) + log.debug(updated) tools = Tools.update_tool_by_id(id, updated) if tools: @@ -343,7 +348,7 @@ async def update_tools_valves_by_id( Tools.update_tool_valves_by_id(id, valves.model_dump()) return valves.model_dump() except Exception as e: - print(e) + log.exception(f"Failed to update tool valves by id {id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(str(e)), @@ -421,7 +426,7 @@ async def update_tools_user_valves_by_id( ) return user_valves.model_dump() except Exception as e: - print(e) + log.exception(f"Failed to update user valves by id {id}: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(str(e)), diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index fb1dc82725..b64adafb44 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -1,4 +1,5 @@ import black +import logging import markdown from open_webui.models.chats import ChatTitleMessagesForm @@ -13,8 +14,12 @@ from open_webui.utils.misc import get_gravatar_url from open_webui.utils.pdf_generator import PDFGenerator from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.code_interpreter import execute_code_jupyter +from open_webui.env import SRC_LOG_LEVELS +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) + router = APIRouter() @@ -96,7 +101,7 @@ async def download_chat_as_pdf( headers={"Content-Disposition": "attachment;filename=chat.pdf"}, ) except Exception as e: - print(e) + log.exception(f"Error generating PDF: {e}") raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 160a45153a..2f31cbdafb 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -1,10 +1,12 @@ import os import shutil import json +import logging from abc import ABC, abstractmethod from typing import BinaryIO, Tuple import boto3 +from botocore.config import Config from botocore.exceptions import ClientError from open_webui.config import ( S3_ACCESS_KEY_ID, @@ -13,6 +15,8 @@ from open_webui.config import ( S3_KEY_PREFIX, S3_REGION_NAME, S3_SECRET_ACCESS_KEY, + S3_USE_ACCELERATE_ENDPOINT, + S3_ADDRESSING_STYLE, GCS_BUCKET_NAME, GOOGLE_APPLICATION_CREDENTIALS_JSON, AZURE_STORAGE_ENDPOINT, @@ -27,6 +31,11 @@ from open_webui.constants import ERROR_MESSAGES from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient from azure.core.exceptions import ResourceNotFoundError +from open_webui.env import SRC_LOG_LEVELS + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) class StorageProvider(ABC): @@ -71,7 +80,7 @@ class LocalStorageProvider(StorageProvider): if os.path.isfile(file_path): os.remove(file_path) else: - print(f"File {file_path} not found in local storage.") + log.warning(f"File {file_path} not found in local storage.") @staticmethod def delete_all_files() -> None: @@ -85,9 +94,9 @@ class LocalStorageProvider(StorageProvider): elif os.path.isdir(file_path): shutil.rmtree(file_path) # Remove the directory except Exception as e: - print(f"Failed to delete {file_path}. Reason: {e}") + log.exception(f"Failed to delete {file_path}. Reason: {e}") else: - print(f"Directory {UPLOAD_DIR} not found in local storage.") + log.warning(f"Directory {UPLOAD_DIR} not found in local storage.") class S3StorageProvider(StorageProvider): @@ -98,6 +107,12 @@ class S3StorageProvider(StorageProvider): endpoint_url=S3_ENDPOINT_URL, aws_access_key_id=S3_ACCESS_KEY_ID, aws_secret_access_key=S3_SECRET_ACCESS_KEY, + config=Config( + s3={ + "use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT, + "addressing_style": S3_ADDRESSING_STYLE, + }, + ), ) self.bucket_name = S3_BUCKET_NAME self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else "" diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py new file mode 100644 index 0000000000..2d7ceabcb8 --- /dev/null +++ b/backend/open_webui/utils/audit.py @@ -0,0 +1,249 @@ +from contextlib import asynccontextmanager +from dataclasses import asdict, dataclass +from enum import Enum +import re +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Dict, + MutableMapping, + Optional, + cast, +) +import uuid + +from asgiref.typing import ( + ASGI3Application, + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + Scope as ASGIScope, +) +from loguru import logger +from starlette.requests import Request + +from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE +from open_webui.utils.auth import get_current_user, get_http_authorization_cred +from open_webui.models.users import UserModel + + +if TYPE_CHECKING: + from loguru import Logger + + +@dataclass(frozen=True) +class AuditLogEntry: + # `Metadata` audit level properties + id: str + user: dict[str, Any] + audit_level: str + verb: str + request_uri: str + user_agent: Optional[str] = None + source_ip: Optional[str] = None + # `Request` audit level properties + request_object: Any = None + # `Request Response` level + response_object: Any = None + response_status_code: Optional[int] = None + + +class AuditLevel(str, Enum): + NONE = "NONE" + METADATA = "METADATA" + REQUEST = "REQUEST" + REQUEST_RESPONSE = "REQUEST_RESPONSE" + + +class AuditLogger: + """ + A helper class that encapsulates audit logging functionality. It uses Loguru’s logger with an auditable binding to ensure that audit log entries are filtered correctly. + + Parameters: + logger (Logger): An instance of Loguru’s logger. + """ + + def __init__(self, logger: "Logger"): + self.logger = logger.bind(auditable=True) + + def write( + self, + audit_entry: AuditLogEntry, + *, + log_level: str = "INFO", + extra: Optional[dict] = None, + ): + + entry = asdict(audit_entry) + + if extra: + entry["extra"] = extra + + self.logger.log( + log_level, + "", + **entry, + ) + + +class AuditContext: + """ + Captures and aggregates the HTTP request and response bodies during the processing of a request. It ensures that only a configurable maximum amount of data is stored to prevent excessive memory usage. + + Attributes: + request_body (bytearray): Accumulated request payload. + response_body (bytearray): Accumulated response payload. + max_body_size (int): Maximum number of bytes to capture. + metadata (Dict[str, Any]): A dictionary to store additional audit metadata (user, http verb, user agent, etc.). + """ + + def __init__(self, max_body_size: int = MAX_BODY_LOG_SIZE): + self.request_body = bytearray() + self.response_body = bytearray() + self.max_body_size = max_body_size + self.metadata: Dict[str, Any] = {} + + def add_request_chunk(self, chunk: bytes): + if len(self.request_body) < self.max_body_size: + self.request_body.extend( + chunk[: self.max_body_size - len(self.request_body)] + ) + + def add_response_chunk(self, chunk: bytes): + if len(self.response_body) < self.max_body_size: + self.response_body.extend( + chunk[: self.max_body_size - len(self.response_body)] + ) + + +class AuditLoggingMiddleware: + """ + ASGI middleware that intercepts HTTP requests and responses to perform audit logging. It captures request/response bodies (depending on audit level), headers, HTTP methods, and user information, then logs a structured audit entry at the end of the request cycle. + """ + + AUDITED_METHODS = {"PUT", "PATCH", "DELETE", "POST"} + + def __init__( + self, + app: ASGI3Application, + *, + excluded_paths: Optional[list[str]] = None, + max_body_size: int = MAX_BODY_LOG_SIZE, + audit_level: AuditLevel = AuditLevel.NONE, + ) -> None: + self.app = app + self.audit_logger = AuditLogger(logger) + self.excluded_paths = excluded_paths or [] + self.max_body_size = max_body_size + self.audit_level = audit_level + + async def __call__( + self, + scope: ASGIScope, + receive: ASGIReceiveCallable, + send: ASGISendCallable, + ) -> None: + if scope["type"] != "http": + return await self.app(scope, receive, send) + + request = Request(scope=cast(MutableMapping, scope)) + + if self._should_skip_auditing(request): + return await self.app(scope, receive, send) + + async with self._audit_context(request) as context: + + async def send_wrapper(message: ASGISendEvent) -> None: + if self.audit_level == AuditLevel.REQUEST_RESPONSE: + await self._capture_response(message, context) + + await send(message) + + original_receive = receive + + async def receive_wrapper() -> ASGIReceiveEvent: + nonlocal original_receive + message = await original_receive() + + if self.audit_level in ( + AuditLevel.REQUEST, + AuditLevel.REQUEST_RESPONSE, + ): + await self._capture_request(message, context) + + return message + + await self.app(scope, receive_wrapper, send_wrapper) + + @asynccontextmanager + async def _audit_context( + self, request: Request + ) -> AsyncGenerator[AuditContext, None]: + """ + async context manager that ensures that an audit log entry is recorded after the request is processed. + """ + context = AuditContext() + try: + yield context + finally: + await self._log_audit_entry(request, context) + + async def _get_authenticated_user(self, request: Request) -> UserModel: + + auth_header = request.headers.get("Authorization") + assert auth_header + user = get_current_user(request, None, get_http_authorization_cred(auth_header)) + + return user + + def _should_skip_auditing(self, request: Request) -> bool: + if ( + request.method not in {"POST", "PUT", "PATCH", "DELETE"} + or AUDIT_LOG_LEVEL == "NONE" + or not request.headers.get("authorization") + ): + return True + # match either /api//...(for the endpoint /api/chat case) or /api/v1//... + pattern = re.compile( + r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b" + ) + if pattern.match(request.url.path): + return True + + return False + + async def _capture_request(self, message: ASGIReceiveEvent, context: AuditContext): + if message["type"] == "http.request": + body = message.get("body", b"") + context.add_request_chunk(body) + + async def _capture_response(self, message: ASGISendEvent, context: AuditContext): + if message["type"] == "http.response.start": + context.metadata["response_status_code"] = message["status"] + + elif message["type"] == "http.response.body": + body = message.get("body", b"") + context.add_response_chunk(body) + + async def _log_audit_entry(self, request: Request, context: AuditContext): + try: + user = await self._get_authenticated_user(request) + + entry = AuditLogEntry( + id=str(uuid.uuid4()), + user=user.model_dump(include={"id", "name", "email", "role"}), + audit_level=self.audit_level.value, + verb=request.method, + request_uri=str(request.url), + response_status_code=context.metadata.get("response_status_code", None), + source_ip=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent"), + request_object=context.request_body.decode("utf-8", errors="replace"), + response_object=context.response_body.decode("utf-8", errors="replace"), + ) + + self.audit_logger.write(entry) + except Exception as e: + logger.error(f"Failed to log audit entry: {str(e)}") diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index e478284a69..cbc8b15aed 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -14,14 +14,22 @@ from typing import Optional, Union, List, Dict from open_webui.models.users import Users from open_webui.constants import ERROR_MESSAGES -from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY, STATIC_DIR +from open_webui.env import ( + WEBUI_SECRET_KEY, + TRUSTED_SIGNATURE_KEY, + STATIC_DIR, + SRC_LOG_LEVELS, +) -from fastapi import Depends, HTTPException, Request, Response, status +from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from passlib.context import CryptContext + logging.getLogger("passlib").setLevel(logging.ERROR) +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OAUTH"]) SESSION_SECRET = WEBUI_SECRET_KEY ALGORITHM = "HS256" @@ -50,7 +58,7 @@ def verify_signature(payload: str, signature: str) -> bool: def override_static(path: str, content: str): # Ensure path is safe if "/" in path or ".." in path: - print(f"Invalid path: {path}") + log.error(f"Invalid path: {path}") return file_path = os.path.join(STATIC_DIR, path) @@ -82,11 +90,11 @@ def get_license_data(app, key): return True else: - print( + log.error( f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}" ) except Exception as ex: - print(f"License: Uncaught Exception: {ex}") + log.exception(f"License: Uncaught Exception: {ex}") return False @@ -142,6 +150,7 @@ def get_http_authorization_cred(auth_header: str): def get_current_user( request: Request, + background_tasks: BackgroundTasks, auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), ): token = None @@ -194,7 +203,10 @@ def get_current_user( detail=ERROR_MESSAGES.INVALID_TOKEN, ) else: - Users.update_user_last_active_by_id(user.id) + # Refresh the user's last active timestamp asynchronously + # to prevent blocking the request + if background_tasks: + background_tasks.add_task(Users.update_user_last_active_by_id, user.id) return user else: raise HTTPException( diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 739d6515e4..74d0af4f7a 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -66,7 +66,7 @@ async def generate_direct_chat_completion( user: Any, models: dict, ): - print("generate_direct_chat_completion") + log.info("generate_direct_chat_completion") metadata = form_data.pop("metadata", {}) @@ -103,7 +103,7 @@ async def generate_direct_chat_completion( } ) - print("res", res) + log.info(f"res: {res}") if res.get("status", False): # Define a generator to stream responses @@ -432,7 +432,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A ) ) except Exception as e: - print(e) + log.exception(f"Failed to get user values: {e}") params = {**params, "__user__": __user__} diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index de51bd46e5..0ca754ed8d 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -1,6 +1,12 @@ import inspect +import logging + from open_webui.utils.plugin import load_function_module_by_id from open_webui.models.functions import Functions +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) def get_sorted_filter_ids(model): @@ -61,7 +67,12 @@ async def process_filter_functions( try: # Prepare parameters sig = inspect.signature(handler) - params = {"body": form_data} | { + + params = {"body": form_data} + if filter_type == "stream": + params = {"event": form_data} + + params = params | { k: v for k, v in { **extra_params, @@ -80,7 +91,7 @@ async def process_filter_functions( ) ) except Exception as e: - print(e) + log.exception(f"Failed to get user values: {e}") # Execute handler if inspect.iscoroutinefunction(handler): @@ -89,7 +100,7 @@ async def process_filter_functions( form_data = handler(**params) except Exception as e: - print(f"Error in {filter_type} handler {filter_id}: {e}") + log.exception(f"Error in {filter_type} handler {filter_id}: {e}") raise e # Handle file cleanup for inlet diff --git a/backend/open_webui/utils/logger.py b/backend/open_webui/utils/logger.py new file mode 100644 index 0000000000..2557610060 --- /dev/null +++ b/backend/open_webui/utils/logger.py @@ -0,0 +1,140 @@ +import json +import logging +import sys +from typing import TYPE_CHECKING + +from loguru import logger + +from open_webui.env import ( + AUDIT_LOG_FILE_ROTATION_SIZE, + AUDIT_LOG_LEVEL, + AUDIT_LOGS_FILE_PATH, + GLOBAL_LOG_LEVEL, +) + + +if TYPE_CHECKING: + from loguru import Record + + +def stdout_format(record: "Record") -> str: + """ + Generates a formatted string for log records that are output to the console. This format includes a timestamp, log level, source location (module, function, and line), the log message, and any extra data (serialized as JSON). + + Parameters: + record (Record): A Loguru record that contains logging details including time, level, name, function, line, message, and any extra context. + Returns: + str: A formatted log string intended for stdout. + """ + record["extra"]["extra_json"] = json.dumps(record["extra"]) + return ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | " + "{level: <8} | " + "{name}:{function}:{line} - " + "{message} - {extra[extra_json]}" + "\n{exception}" + ) + + +class InterceptHandler(logging.Handler): + """ + Intercepts log records from Python's standard logging module + and redirects them to Loguru's logger. + """ + + def emit(self, record): + """ + Called by the standard logging module for each log event. + It transforms the standard `LogRecord` into a format compatible with Loguru + and passes it to Loguru's logger. + """ + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + frame, depth = sys._getframe(6), 6 + while frame and frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log( + level, record.getMessage() + ) + + +def file_format(record: "Record"): + """ + Formats audit log records into a structured JSON string for file output. + + Parameters: + record (Record): A Loguru record containing extra audit data. + Returns: + str: A JSON-formatted string representing the audit data. + """ + + audit_data = { + "id": record["extra"].get("id", ""), + "timestamp": int(record["time"].timestamp()), + "user": record["extra"].get("user", dict()), + "audit_level": record["extra"].get("audit_level", ""), + "verb": record["extra"].get("verb", ""), + "request_uri": record["extra"].get("request_uri", ""), + "response_status_code": record["extra"].get("response_status_code", 0), + "source_ip": record["extra"].get("source_ip", ""), + "user_agent": record["extra"].get("user_agent", ""), + "request_object": record["extra"].get("request_object", b""), + "response_object": record["extra"].get("response_object", b""), + "extra": record["extra"].get("extra", {}), + } + + record["extra"]["file_extra"] = json.dumps(audit_data, default=str) + return "{extra[file_extra]}\n" + + +def start_logger(): + """ + Initializes and configures Loguru's logger with distinct handlers: + + A console (stdout) handler for general log messages (excluding those marked as auditable). + An optional file handler for audit logs if audit logging is enabled. + Additionally, this function reconfigures Python’s standard logging to route through Loguru and adjusts logging levels for Uvicorn. + + Parameters: + enable_audit_logging (bool): Determines whether audit-specific log entries should be recorded to file. + """ + logger.remove() + + logger.add( + sys.stdout, + level=GLOBAL_LOG_LEVEL, + format=stdout_format, + filter=lambda record: "auditable" not in record["extra"], + ) + + if AUDIT_LOG_LEVEL != "NONE": + try: + logger.add( + AUDIT_LOGS_FILE_PATH, + level="INFO", + rotation=AUDIT_LOG_FILE_ROTATION_SIZE, + compression="zip", + format=file_format, + filter=lambda record: record["extra"].get("auditable") is True, + ) + except Exception as e: + logger.error(f"Failed to initialize audit log file handler: {str(e)}") + + logging.basicConfig( + handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True + ) + for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]: + uvicorn_logger = logging.getLogger(uvicorn_logger_name) + uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL) + uvicorn_logger.handlers = [] + for uvicorn_logger_name in ["uvicorn.access"]: + uvicorn_logger = logging.getLogger(uvicorn_logger_name) + uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL) + uvicorn_logger.handlers = [InterceptHandler()] + + logger.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 7ec764fc01..43fd0d480a 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -351,24 +351,25 @@ async def chat_web_search_handler( all_results.append(results) files = form_data.get("files", []) - if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT: - files.append( - { - "docs": results.get("docs", []), - "name": searchQuery, - "type": "web_search_docs", - "urls": results["filenames"], - } - ) - else: + if results.get("collection_name"): files.append( { "collection_name": results["collection_name"], "name": searchQuery, - "type": "web_search_results", + "type": "web_search", "urls": results["filenames"], } ) + elif results.get("docs"): + files.append( + { + "docs": results.get("docs", []), + "name": searchQuery, + "type": "web_search", + "urls": results["filenames"], + } + ) + form_data["files"] = files except Exception as e: log.exception(e) @@ -518,6 +519,7 @@ async def chat_completion_files_handler( sources = [] if files := body.get("metadata", {}).get("files", None): + queries = [] try: queries_response = await generate_queries( request, @@ -543,8 +545,8 @@ async def chat_completion_files_handler( queries_response = {"queries": [queries_response]} queries = queries_response.get("queries", []) - except Exception as e: - queries = [] + except: + pass if len(queries) == 0: queries = [get_last_user_message(body["messages"])] @@ -556,6 +558,7 @@ async def chat_completion_files_handler( sources = await loop.run_in_executor( executor, lambda: get_sources_from_files( + request=request, files=files, queries=queries, embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION( @@ -738,6 +741,7 @@ async def process_chat_payload(request, form_data, metadata, user, model): tool_ids = form_data.pop("tool_ids", None) files = form_data.pop("files", None) + # Remove files duplicates if files: files = list({json.dumps(f, sort_keys=True): f for f in files}.values()) @@ -795,8 +799,6 @@ async def process_chat_payload(request, form_data, metadata, user, model): if len(sources) > 0: context_string = "" for source_idx, source in enumerate(sources): - source_id = source.get("source", {}).get("name", "") - if "document" in source: for doc_idx, doc_context in enumerate(source["document"]): context_string += f"{source_idx}{doc_context}\n" @@ -1048,6 +1050,21 @@ async def process_chat_response( ): return response + extra_params = { + "__event_emitter__": event_emitter, + "__event_call__": event_caller, + "__user__": { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + }, + "__metadata__": metadata, + "__request__": request, + "__model__": metadata.get("model"), + } + filter_ids = get_sorted_filter_ids(form_data.get("model")) + # Streaming response if event_emitter and event_caller: task_id = str(uuid4()) # Create a unique task ID. @@ -1127,12 +1144,12 @@ async def process_chat_response( if reasoning_duration is not None: if raw: - content = f'{content}\n<{block["tag"]}>{block["content"]}\n' + content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n' else: content = f'{content}\n
\nThought for {reasoning_duration} seconds\n{reasoning_display_content}\n
\n' else: if raw: - content = f'{content}\n<{block["tag"]}>{block["content"]}\n' + content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n' else: content = f'{content}\n
\nThinking…\n{reasoning_display_content}\n
\n' @@ -1228,9 +1245,9 @@ async def process_chat_response( return attributes if content_blocks[-1]["type"] == "text": - for tag in tags: + for start_tag, end_tag in tags: # Match start tag e.g., or - start_tag_pattern = rf"<{tag}(\s.*?)?>" + start_tag_pattern = rf"<{re.escape(start_tag)}(\s.*?)?>" match = re.search(start_tag_pattern, content) if match: attr_content = ( @@ -1263,7 +1280,8 @@ async def process_chat_response( content_blocks.append( { "type": content_type, - "tag": tag, + "start_tag": start_tag, + "end_tag": end_tag, "attributes": attributes, "content": "", "started_at": time.time(), @@ -1275,9 +1293,10 @@ async def process_chat_response( break elif content_blocks[-1]["type"] == content_type: - tag = content_blocks[-1]["tag"] + start_tag = content_blocks[-1]["start_tag"] + end_tag = content_blocks[-1]["end_tag"] # Match end tag e.g., - end_tag_pattern = rf"" + end_tag_pattern = rf"<{re.escape(end_tag)}>" # Check if the content has the end tag if re.search(end_tag_pattern, content): @@ -1285,7 +1304,7 @@ async def process_chat_response( block_content = content_blocks[-1]["content"] # Strip start and end tags from the content - start_tag_pattern = rf"<{tag}(.*?)>" + start_tag_pattern = rf"<{re.escape(start_tag)}(.*?)>" block_content = re.sub( start_tag_pattern, "", block_content ).strip() @@ -1350,7 +1369,7 @@ async def process_chat_response( # Clean processed content content = re.sub( - rf"<{tag}(.*?)>(.|\n)*?", + rf"<{re.escape(start_tag)}(.*?)>(.|\n)*?<{re.escape(end_tag)}>", "", content, flags=re.DOTALL, @@ -1388,19 +1407,24 @@ async def process_chat_response( # We might want to disable this by default DETECT_REASONING = True + DETECT_SOLUTION = True DETECT_CODE_INTERPRETER = metadata.get("features", {}).get( "code_interpreter", False ) reasoning_tags = [ - "think", - "thinking", - "reason", - "reasoning", - "thought", - "Thought", + ("think", "/think"), + ("thinking", "/thinking"), + ("reason", "/reason"), + ("reasoning", "/reasoning"), + ("thought", "/thought"), + ("Thought", "/Thought"), + ("|begin_of_thought|", "|end_of_thought|"), ] - code_interpreter_tags = ["code_interpreter"] + + code_interpreter_tags = [("code_interpreter", "/code_interpreter")] + + solution_tags = [("|begin_of_solution|", "|end_of_solution|")] try: for event in events: @@ -1444,119 +1468,154 @@ async def process_chat_response( try: data = json.loads(data) - if "selected_model_id" in data: - model_id = data["selected_model_id"] - Chats.upsert_message_to_chat_by_id_and_message_id( - metadata["chat_id"], - metadata["message_id"], - { - "selectedModelId": model_id, - }, - ) - else: - choices = data.get("choices", []) - if not choices: - continue + data, _ = await process_filter_functions( + request=request, + filter_ids=filter_ids, + filter_type="stream", + form_data=data, + extra_params=extra_params, + ) - delta = choices[0].get("delta", {}) - delta_tool_calls = delta.get("tool_calls", None) - - if delta_tool_calls: - for delta_tool_call in delta_tool_calls: - tool_call_index = delta_tool_call.get("index") - - if tool_call_index is not None: - if ( - len(response_tool_calls) - <= tool_call_index - ): - response_tool_calls.append( - delta_tool_call - ) - else: - delta_name = delta_tool_call.get( - "function", {} - ).get("name") - delta_arguments = delta_tool_call.get( - "function", {} - ).get("arguments") - - if delta_name: - response_tool_calls[ - tool_call_index - ]["function"]["name"] += delta_name - - if delta_arguments: - response_tool_calls[ - tool_call_index - ]["function"][ - "arguments" - ] += delta_arguments - - value = delta.get("content") - - if value: - content = f"{content}{value}" - - if not content_blocks: - content_blocks.append( - { - "type": "text", - "content": "", - } - ) - - content_blocks[-1]["content"] = ( - content_blocks[-1]["content"] + value + if data: + if "selected_model_id" in data: + model_id = data["selected_model_id"] + Chats.upsert_message_to_chat_by_id_and_message_id( + metadata["chat_id"], + metadata["message_id"], + { + "selectedModelId": model_id, + }, ) - - if DETECT_REASONING: - content, content_blocks, _ = ( - tag_content_handler( - "reasoning", - reasoning_tags, - content, - content_blocks, + else: + choices = data.get("choices", []) + if not choices: + usage = data.get("usage", {}) + if usage: + await event_emitter( + { + "type": "chat:completion", + "data": { + "usage": usage, + }, + } ) + continue + + delta = choices[0].get("delta", {}) + delta_tool_calls = delta.get("tool_calls", None) + + if delta_tool_calls: + for delta_tool_call in delta_tool_calls: + tool_call_index = delta_tool_call.get( + "index" + ) + + if tool_call_index is not None: + if ( + len(response_tool_calls) + <= tool_call_index + ): + response_tool_calls.append( + delta_tool_call + ) + else: + delta_name = delta_tool_call.get( + "function", {} + ).get("name") + delta_arguments = ( + delta_tool_call.get( + "function", {} + ).get("arguments") + ) + + if delta_name: + response_tool_calls[ + tool_call_index + ]["function"][ + "name" + ] += delta_name + + if delta_arguments: + response_tool_calls[ + tool_call_index + ]["function"][ + "arguments" + ] += delta_arguments + + value = delta.get("content") + + if value: + content = f"{content}{value}" + + if not content_blocks: + content_blocks.append( + { + "type": "text", + "content": "", + } + ) + + content_blocks[-1]["content"] = ( + content_blocks[-1]["content"] + value ) - if DETECT_CODE_INTERPRETER: - content, content_blocks, end = ( - tag_content_handler( - "code_interpreter", - code_interpreter_tags, - content, - content_blocks, + if DETECT_REASONING: + content, content_blocks, _ = ( + tag_content_handler( + "reasoning", + reasoning_tags, + content, + content_blocks, + ) ) - ) - if end: - break + if DETECT_CODE_INTERPRETER: + content, content_blocks, end = ( + tag_content_handler( + "code_interpreter", + code_interpreter_tags, + content, + content_blocks, + ) + ) - if ENABLE_REALTIME_CHAT_SAVE: - # Save message in the database - Chats.upsert_message_to_chat_by_id_and_message_id( - metadata["chat_id"], - metadata["message_id"], - { + if end: + break + + if DETECT_SOLUTION: + content, content_blocks, _ = ( + tag_content_handler( + "solution", + solution_tags, + content, + content_blocks, + ) + ) + + if ENABLE_REALTIME_CHAT_SAVE: + # Save message in the database + Chats.upsert_message_to_chat_by_id_and_message_id( + metadata["chat_id"], + metadata["message_id"], + { + "content": serialize_content_blocks( + content_blocks + ), + }, + ) + else: + data = { "content": serialize_content_blocks( content_blocks ), - }, - ) - else: - data = { - "content": serialize_content_blocks( - content_blocks - ), - } + } - await event_emitter( - { - "type": "chat:completion", - "data": data, - } - ) + await event_emitter( + { + "type": "chat:completion", + "data": data, + } + ) except Exception as e: done = "data: [DONE]" in line if done: @@ -1855,7 +1914,10 @@ async def process_chat_response( } ) - print(content_blocks, serialize_content_blocks(content_blocks)) + log.info(f"content_blocks={content_blocks}") + log.info( + f"serialize_content_blocks={serialize_content_blocks(content_blocks)}" + ) try: res = await generate_chat_completion( @@ -1926,7 +1988,7 @@ async def process_chat_response( await background_tasks_handler() except asyncio.CancelledError: - print("Task was cancelled!") + log.warning("Task was cancelled!") await event_emitter({"type": "task-cancelled"}) if not ENABLE_REALTIME_CHAT_SAVE: @@ -1947,17 +2009,34 @@ async def process_chat_response( return {"status": True, "task_id": task_id} else: - # Fallback to the original response async def stream_wrapper(original_generator, events): def wrap_item(item): return f"data: {item}\n\n" for event in events: - yield wrap_item(json.dumps(event)) + event, _ = await process_filter_functions( + request=request, + filter_ids=filter_ids, + filter_type="stream", + form_data=event, + extra_params=extra_params, + ) + + if event: + yield wrap_item(json.dumps(event)) async for data in original_generator: - yield data + data, _ = await process_filter_functions( + request=request, + filter_ids=filter_ids, + filter_type="stream", + form_data=data, + extra_params=extra_params, + ) + + if data: + yield data return StreamingResponse( stream_wrapper(response.body_iterator, events), diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index f79b626843..8f867bacea 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -2,12 +2,17 @@ import hashlib import re import time import uuid +import logging from datetime import timedelta from pathlib import Path from typing import Callable, Optional import collections.abc +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) def deep_update(d, u): @@ -412,7 +417,7 @@ def parse_ollama_modelfile(model_text): elif param_type is bool: value = value.lower() == "true" except Exception as e: - print(e) + log.exception(f"Failed to parse parameter {param}: {e}") continue data["params"][param] = value diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 13835e7847..2af54c19d7 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -146,7 +146,7 @@ class OAuthManager: nested_claims = oauth_claim.split(".") for nested_claim in nested_claims: claim_data = claim_data.get(nested_claim, {}) - user_oauth_groups = claim_data if isinstance(claim_data, list) else None + user_oauth_groups = claim_data if isinstance(claim_data, list) else [] user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id) all_available_groups: list[GroupModel] = Groups.get_groups() @@ -315,15 +315,6 @@ class OAuthManager: if not user: user_count = Users.get_num_users() - if ( - request.app.state.USER_COUNT - and user_count >= request.app.state.USER_COUNT - ): - raise HTTPException( - 403, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - # If the user does not exist, check if signups are enabled if auth_manager_config.ENABLE_OAUTH_SIGNUP: # Check if an existing user with the same email already exists diff --git a/backend/open_webui/utils/payload.py b/backend/open_webui/utils/payload.py index 51e8d50ccb..869e708955 100644 --- a/backend/open_webui/utils/payload.py +++ b/backend/open_webui/utils/payload.py @@ -124,7 +124,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: tool_call_id = message.get("tool_call_id", None) # Check if the content is a string (just a simple message) - if isinstance(content, str): + if isinstance(content, str) and not tool_calls: # If the content is a string, it's pure text new_message["content"] = content @@ -230,6 +230,12 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict: "system" ] # To prevent Ollama warning of invalid option provided + # If there is the "stop" parameter in the openai_payload, remap it to the ollama_payload.options + if "stop" in openai_payload: + ollama_options = ollama_payload.get("options", {}) + ollama_options["stop"] = openai_payload.get("stop") + ollama_payload["options"] = ollama_options + if "metadata" in openai_payload: ollama_payload["metadata"] = openai_payload["metadata"] diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index d6e24d6b93..e3fe9237f5 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -45,7 +45,7 @@ def extract_frontmatter(content): frontmatter[key.strip()] = value.strip() except Exception as e: - print(f"An error occurred: {e}") + log.exception(f"Failed to extract frontmatter: {e}") return {} return frontmatter diff --git a/backend/open_webui/utils/response.py b/backend/open_webui/utils/response.py index bc47e1e136..8c3f1a58eb 100644 --- a/backend/open_webui/utils/response.py +++ b/backend/open_webui/utils/response.py @@ -104,7 +104,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response) data = json.loads(data) model = data.get("model", "ollama") - message_content = data.get("message", {}).get("content", "") + message_content = data.get("message", {}).get("content", None) tool_calls = data.get("message", {}).get("tool_calls", None) openai_tool_calls = None @@ -118,7 +118,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response) usage = convert_ollama_usage_to_openai(data) data = openai_chat_chunk_message_template( - model, message_content if not done else None, openai_tool_calls, usage + model, message_content, openai_tool_calls, usage ) line = f"data: {json.dumps(data)}\n\n" diff --git a/backend/requirements.txt b/backend/requirements.txt index 965741f781..616827144c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -31,6 +31,9 @@ APScheduler==3.10.4 RestrictedPython==8.0 +loguru==0.7.2 +asgiref==3.8.1 + # AI libraries openai anthropic @@ -71,6 +74,7 @@ validators==0.34.0 psutil sentencepiece soundfile==0.13.1 +azure-ai-documentintelligence==1.0.0 opencv-python-headless==4.11.0.86 rapidocr-onnxruntime==1.3.24 diff --git a/package-lock.json b/package-lock.json index c658707721..1ce7424f5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.5.16", + "version": "0.5.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.5.16", + "version": "0.5.17", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", diff --git a/package.json b/package.json index 86568869ff..a5db14e190 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.5.16", + "version": "0.5.17", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 5cd54da64f..ccf4863462 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ dependencies = [ "RestrictedPython==8.0", + "loguru==0.7.2", + "asgiref==3.8.1", + "openai", "anthropic", "google-generativeai==0.7.2", @@ -78,6 +81,7 @@ dependencies = [ "psutil", "sentencepiece", "soundfile==0.13.1", + "azure-ai-documentintelligence==1.0.0", "opencv-python-headless==4.11.0.86", "rapidocr-onnxruntime==1.3.24", diff --git a/src/lib/apis/retrieval/index.ts b/src/lib/apis/retrieval/index.ts index c35c37847b..31317fe0b9 100644 --- a/src/lib/apis/retrieval/index.ts +++ b/src/lib/apis/retrieval/index.ts @@ -32,9 +32,15 @@ type ChunkConfigForm = { chunk_overlap: number; }; +type DocumentIntelligenceConfigForm = { + key: string; + endpoint: string; +}; + type ContentExtractConfigForm = { engine: string; tika_server_url: string | null; + document_intelligence_config: DocumentIntelligenceConfigForm | null; }; type YoutubeConfigForm = { @@ -46,6 +52,7 @@ type YoutubeConfigForm = { type RAGConfigForm = { pdf_extract_images?: boolean; enable_google_drive_integration?: boolean; + enable_onedrive_integration?: boolean; chunk?: ChunkConfigForm; content_extraction?: ContentExtractConfigForm; web_loader_ssl_verification?: boolean; diff --git a/src/lib/components/admin/Functions/FunctionEditor.svelte b/src/lib/components/admin/Functions/FunctionEditor.svelte index cbdec24257..6da2a83f45 100644 --- a/src/lib/components/admin/Functions/FunctionEditor.svelte +++ b/src/lib/components/admin/Functions/FunctionEditor.svelte @@ -1,8 +1,7 @@ @@ -293,473 +318,479 @@ }} >
-
-
{$i18n.t('General Settings')}
- -
-
{$i18n.t('Embedding Model Engine')}
-
- -
-
- - {#if embeddingEngine === 'openai'} -
- - - -
- {:else if embeddingEngine === 'ollama'} -
- - - -
- {/if} - - {#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'} -
-
{$i18n.t('Embedding Batch Size')}
-
- -
-
- -
-
- {/if} - -
-
{$i18n.t('Hybrid Search')}
- - -
- -
-
{$i18n.t('Full Context Mode')}
-
- - - -
-
-
- -
- -
-
-
{$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 querySettings.hybrid === true} -
-
{$i18n.t('Reranking Model')}
- -
-
- -
- -
-
- {/if} -
- -
-
-
{$i18n.t('Content Extraction')}
+
+
{$i18n.t('General')}
-
-
{$i18n.t('Engine')}
-
- -
-
+
- {#if showTikaServerUrl} -
-
- -
-
- {/if} -
- -
- -
{$i18n.t('Google Drive')}
- -
-
-
{$i18n.t('Enable Google Drive')}
-
- -
-
-
- -
- -
-
{$i18n.t('Query Params')}
- -
-
-
{$i18n.t('Top K')}
- -
- -
-
- - {#if querySettings.hybrid === true} -
-
- {$i18n.t('Minimum Score')} +
+
+
+ {$i18n.t('Content Extraction Engine')}
-
+
+ +
+
+ {#if contentExtractionEngine === 'tika'} +
+
+ +
+
+ {:else if contentExtractionEngine === 'document_intelligence'} +
+ + +
+ {/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 querySettings.hybrid === true} -
- {$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 !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 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('Full Context Mode')}
+
+ + + +
+
+ +
+
{$i18n.t('Hybrid Search')}
+
+ { + toggleHybridSearch(); + }} + /> +
+
+ + {#if querySettings.hybrid === true} +
+
{$i18n.t('Reranking Model')}
+ +
+
+
+ +
+ +
+
+
+ {/if} +
+ +
+
{$i18n.t('Retrieval')}
+ +
+ +
+
{$i18n.t('Top K')}
+
+ +
+
+ + {#if querySettings.hybrid === true} +
+
+
{$i18n.t('Minimum Score')}
+
+ +
+
+
+ {$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} + +
+
{$i18n.t('RAG Template')}
+
+ +