diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 8c09b5ce82..3a7208a317 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -57,9 +57,7 @@ class EndpointFilter(logging.Filter): logging.getLogger('uvicorn.access').addFilter(EndpointFilter()) -#################################### # Initialization -#################################### def run_migrations(): @@ -92,9 +90,7 @@ async def import_legacy_config_json(): os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json') -#################################### # Static DIR -#################################### STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')).resolve() STATIC_SOURCE_DIR = FRONTEND_BUILD_DIR / 'static' @@ -151,7 +147,7 @@ if frontend_loader.exists(): logging.error(f'An error occurred: {e}') -# --- Storage Provider --- +# Storage provider STORAGE_PROVIDER = os.getenv('STORAGE_PROVIDER', 'local') # defaults to local, s3 STORAGE_LOCAL_CACHE = os.getenv('STORAGE_LOCAL_CACHE', 'true').lower() == 'true' @@ -173,25 +169,19 @@ AZURE_STORAGE_ENDPOINT = os.getenv('AZURE_STORAGE_ENDPOINT', None) AZURE_STORAGE_CONTAINER_NAME = os.getenv('AZURE_STORAGE_CONTAINER_NAME', None) AZURE_STORAGE_KEY = os.getenv('AZURE_STORAGE_KEY', None) -#################################### # File Upload DIR -#################################### UPLOAD_DIR = DATA_DIR / 'uploads' UPLOAD_DIR.mkdir(parents=True, exist_ok=True) -#################################### # Cache DIR -#################################### CACHE_DIR = DATA_DIR / 'cache' CACHE_DIR.mkdir(parents=True, exist_ok=True) -#################################### # CUSTOM_NAME (Legacy) -#################################### # LICENSE covers this legacy Open WebUI branding path. # Do not alter, remove, obscure, or replace it except as LICENSE permits: @@ -229,16 +219,12 @@ if CUSTOM_NAME: pass -#################################### -# DIRECT CONNECTIONS -#################################### +# Direct connections ENABLE_DIRECT_CONNECTIONS = os.getenv('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true' ENABLE_DIRECT_INTEGRATIONS = os.getenv('ENABLE_DIRECT_INTEGRATIONS', 'False').lower() == 'true' -#################################### # OLLAMA_BASE_URL -#################################### ENABLE_OLLAMA_API = os.getenv('ENABLE_OLLAMA_API', 'True').lower() == 'true' @@ -246,7 +232,7 @@ OLLAMA_API_BASE_URL = os.getenv('OLLAMA_API_BASE_URL', 'http://localhost:11434/a OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', '') if OLLAMA_BASE_URL: - # Remove trailing slash + # Trim the trailing slash; the next path will bring its own. OLLAMA_BASE_URL = OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith('/') else OLLAMA_BASE_URL @@ -317,9 +303,7 @@ if _ollama_api_configs: except (JSONCodec.JSONDecodeError, TypeError): log.warning('OLLAMA_API_CONFIGS is not valid JSON, ignoring') -#################################### # OPENAI_API -#################################### ENABLE_OPENAI_API = os.getenv('ENABLE_OPENAI_API', 'True').lower() == 'true' @@ -373,16 +357,12 @@ except Exception: OPENAI_API_BASE_URL = 'https://api.openai.com/v1' -#################################### -# MODELS -#################################### +# Models ENABLE_BASE_MODELS_CACHE = os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true' -#################################### # TOOL_SERVERS -#################################### try: tool_server_connections = JSONCodec.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]')) @@ -395,9 +375,7 @@ TOOL_SERVER_CONNECTIONS = tool_server_connections OAUTH_CLIENT_TIMEOUT = os.getenv('OAUTH_CLIENT_TIMEOUT', '') -#################################### # TERMINAL_SERVER -#################################### terminal_server_connections = JSONCodec.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]')) @@ -408,9 +386,7 @@ try: except Exception: TERMINAL_PROXY_HEADERS = {} -#################################### # Code Interpreter -#################################### ENABLE_CODE_EXECUTION = os.getenv('ENABLE_CODE_EXECUTION', 'True').lower() == 'true' @@ -502,9 +478,7 @@ CODE_INTERPRETER_PYODIDE_PROMPT = """ - Use `import os; os.listdir('/mnt/uploads')` to discover available files.""" -#################################### # Vector Database -#################################### VECTOR_DB = os.getenv('VECTOR_DB', 'pgvector' if USE_SLIM else 'chroma') @@ -841,9 +815,7 @@ VALKEY_HNSW_M = int(os.getenv('VALKEY_HNSW_M', '16')) VALKEY_HNSW_EF_CONSTRUCTION = int(os.getenv('VALKEY_HNSW_EF_CONSTRUCTION', '200')) VALKEY_HNSW_EF_RUNTIME = int(os.getenv('VALKEY_HNSW_EF_RUNTIME', '10')) -#################################### # Information Retrieval (RAG) -#################################### # If configured, Google Drive will be available as an upload option. @@ -1182,9 +1154,7 @@ YOUTUBE_LOADER_LANGUAGE = os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(',') YOUTUBE_LOADER_PROXY_URL = os.getenv('YOUTUBE_LOADER_PROXY_URL', '') -#################################### # Web Search -#################################### ENABLE_WEB_SEARCH = os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true' @@ -1374,9 +1344,7 @@ except JSONCodec.JSONDecodeError: LINKUP_SEARCH_PARAMS = linkup_search_params -#################################### # Images -#################################### ENABLE_IMAGE_GENERATION = os.getenv('ENABLE_IMAGE_GENERATION', '').lower() == 'true' @@ -1584,9 +1552,7 @@ except JSONCodec.JSONDecodeError: IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = images_edit_comfyui_workflow_nodes -#################################### # Audio -#################################### # Transcription WHISPER_MODEL = os.getenv('WHISPER_MODEL', 'base') @@ -1702,9 +1668,7 @@ AUDIO_TTS_MISTRAL_API_KEY = os.getenv('AUDIO_TTS_MISTRAL_API_KEY', '') AUDIO_TTS_MISTRAL_API_BASE_URL = os.getenv('AUDIO_TTS_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1') -#################################### -# WEBUI -#################################### +# WebUI WEBUI_URL = os.getenv('WEBUI_URL', '') @@ -2254,9 +2218,7 @@ SHOW_ADMIN_DETAILS = os.getenv('SHOW_ADMIN_DETAILS', 'true').lower() == 'true' ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', None) -#################################### -# TASKS -#################################### +# Tasks TASK_MODEL = os.getenv('TASK_MODEL', '') @@ -2560,9 +2522,7 @@ Your task is to synthesize these responses into a single, high-quality response. Responses from models: {{responses}}""" -#################################### # Auth -#################################### ENABLE_API_KEYS = os.getenv('ENABLE_API_KEYS', 'False').lower() == 'true' @@ -2588,9 +2548,7 @@ if JWT_EXPIRES_IN == '-1': ' See: https://docs.openwebui.com/reference/env-configuration\n' ) -#################################### # OAuth config -#################################### # Master switch for OAuth/OIDC sign-in. Defaults to enabled so existing # deployments that already have a provider configured keep working; admins can @@ -2898,9 +2856,7 @@ def load_oauth_providers(): load_oauth_providers() -#################################### # LDAP -#################################### ENABLE_LDAP = os.getenv('ENABLE_LDAP', 'false').lower() == 'true' diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 14e882ca42..b672fe0483 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -18,9 +18,7 @@ import markdown from bs4 import BeautifulSoup from cryptography.hazmat.primitives import serialization -#################################### # Load .env file -#################################### # Use .resolve() to get the canonical path, removing any '..' or '.' components ENV_FILE_PATH = Path(__file__).resolve() @@ -72,9 +70,7 @@ if not USE_SLIM and sys.platform == 'darwin' and DEVICE_TYPE == 'cpu': # Torch MPS inference is not thread-safe and a concurrent call kills the whole process. MPS_INFERENCE_LOCK = threading.Lock() if DEVICE_TYPE == 'mps' else nullcontext() -#################################### -# LOGGING -#################################### +# Logging _LEVEL_MAP = { 'DEBUG': 'debug', @@ -132,9 +128,7 @@ if _cuda_error: SRC_LOG_LEVELS = {} # Legacy variable, do not remove -#################################### # ENV (dev,test,prod) -#################################### ENV = os.getenv('ENV', 'dev') @@ -221,9 +215,7 @@ for version in soup.find_all('h2'): CHANGELOG = changelog_json -#################################### -# DATA/FRONTEND BUILD DIR -#################################### +# Data/frontend build dir DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve() @@ -256,9 +248,7 @@ FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).r if FROM_INIT_PY: FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', OPEN_WEBUI_DIR / 'frontend')).resolve() -#################################### # Database -#################################### # Check if the file exists if os.path.exists(f'{DATA_DIR}/ollama.db'): @@ -374,9 +364,7 @@ RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true' # Empty by default: chunk metadata also holds internal bookkeeping (file hashes, collection names, scores). RAG_SOURCE_METADATA_KEYS = [key.strip() for key in os.getenv('RAG_SOURCE_METADATA_KEYS', '').split(',') if key.strip()] -#################################### -# REDIS -#################################### +# Redis REDIS_URL = os.getenv('REDIS_URL', '') REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true' @@ -454,9 +442,7 @@ else: except Exception: REDIS_RECONNECT_DELAY = None -#################################### # Uvicorn -#################################### try: UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1) @@ -466,9 +452,7 @@ except (ValueError, TypeError): # tiny delta-stream frames make per-frame websocket compression CPU-bound, allow opting out (true/false) UVICORN_WS_PER_MESSAGE_DEFLATE = os.getenv('UVICORN_WS_PER_MESSAGE_DEFLATE', 'True').lower() == 'true' -#################################### -# WEBSOCKET SUPPORT -#################################### +# Websocket support ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true' @@ -728,9 +712,7 @@ except (ValueError, TypeError): MCP_INITIALIZE_TIMEOUT = 10 -#################################### # AIOHTTP Connection Pool -#################################### AIOHTTP_POOL_CONNECTIONS = os.getenv('AIOHTTP_POOL_CONNECTIONS', '') if AIOHTTP_POOL_CONNECTIONS == '': @@ -778,18 +760,14 @@ else: RAG_EMBEDDING_TIMEOUT = None -#################################### # Auth -#################################### WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true' ENABLE_INITIAL_ADMIN_SIGNUP = os.getenv('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true' ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.getenv('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true' -#################################### # Secret key & cookies -#################################### # WEBUI_JWT_SECRET_KEY is deprecated; use WEBUI_SECRET_KEY instead. # No hardcoded fallback by design: the supported start scripts set/auto-generate it; unset is rejected below. @@ -823,9 +801,7 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == '': ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true' -#################################### # Admin Account Runtime Creation -#################################### # Optional env vars for creating an admin account on startup # Useful for headless/automated deployments @@ -907,9 +883,7 @@ ENABLE_OPENAI_API_PASSTHROUGH = os.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'Fals WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None) -#################################### # OAUTH Configuration -#################################### ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true' ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true' @@ -944,9 +918,7 @@ OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS = [ # Requires Redis for JWT revocation. ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.getenv('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true' -#################################### # SCIM Configuration -#################################### ENABLE_SCIM = os.getenv('ENABLE_SCIM', os.getenv('SCIM_ENABLED', 'False')).lower() == 'true' SCIM_TOKEN = os.getenv('SCIM_TOKEN', '') @@ -959,9 +931,7 @@ if ENABLE_SCIM and not SCIM_AUTH_PROVIDER: 'to enable externalId storage.' ) -#################################### # LICENSE_KEY -#################################### LICENSE_KEY = os.getenv('LICENSE_KEY', '') @@ -984,9 +954,7 @@ if LICENSE_PUBLIC_KEY: ) -#################################### # WEBUI Identity -#################################### # LICENSE covers this Open WebUI branding surface, including name, logo, # visual, textual, symbolic identifiers, metadata, and surrounding UI. @@ -1004,9 +972,7 @@ WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png' WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build') TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '') -#################################### # Feature flags -#################################### SAFE_MODE = os.getenv('SAFE_MODE', 'False').lower() == 'true' ENABLE_EASTER_EGGS = os.getenv('ENABLE_EASTER_EGGS', 'True').lower() == 'true' @@ -1028,9 +994,7 @@ PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( _profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip() PROFILE_IMAGE_MAX_DATA_URI_SIZE = int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None -#################################### # Forward Headers -#################################### ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true' @@ -1053,9 +1017,7 @@ try: except ValueError: FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = 300 -#################################### # Progressive Web App -#################################### # LICENSE covers this install-time Open WebUI branding surface, including # names, logos, manifests, metadata, and surrounding UI. @@ -1063,18 +1025,14 @@ except ValueError: # https://docs.openwebui.com/license. EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None) -#################################### -# GROUP DEFAULTS -#################################### +# Group defaults # Controls the default "Who can share to this group" setting for new groups. # Env var values: "true" (anyone), "false" (no one), "members" (only group members). _default_group_share = os.getenv('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower() DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true' -#################################### -# MODELS -#################################### +# Models ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true' @@ -1088,9 +1046,7 @@ else: MODELS_CACHE_TTL = 1 -#################################### -# CHAT -#################################### +# Chat ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = ( os.getenv('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true' @@ -1162,9 +1118,7 @@ else: CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None -#################################### -# SENTENCE TRANSFORMERS -#################################### +# Sentence transformers SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '') @@ -1203,9 +1157,7 @@ SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = ( os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true' ) -#################################### -# KNOWLEDGE TOOLS -#################################### +# Knowledge tools def _int_env(name: str, default: int) -> int: @@ -1225,9 +1177,7 @@ KNOWLEDGE_GREP_MAX_MATCHES = _int_env('KNOWLEDGE_GREP_MAX_MATCHES', 50) VIEW_FILE_MAX_CHARS = _int_env('VIEW_FILE_MAX_CHARS', 100_000) VIEW_FILE_DEFAULT_MAX_CHARS = _int_env('VIEW_FILE_DEFAULT_MAX_CHARS', 10_000) -#################################### -# TOOLS/FUNCTIONS PIP OPTIONS -#################################### +# Tools/functions pip options ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true' # Deployment controls: the master switch always overrides all feature switches. @@ -1243,9 +1193,7 @@ PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split() PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split() -#################################### # OFFLINE_MODE -#################################### ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true' OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true' @@ -1254,15 +1202,11 @@ if OFFLINE_MODE: os.environ['HF_HUB_OFFLINE'] = '1' ENABLE_VERSION_UPDATE_CHECK = False -#################################### # Pyodide file persistence -#################################### ENABLE_PYODIDE_FILE_PERSISTENCE = os.getenv('ENABLE_PYODIDE_FILE_PERSISTENCE', 'false').lower() == 'true' -#################################### # Audit logging -#################################### ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true' @@ -1306,9 +1250,7 @@ AUDIT_INCLUDED_PATHS = [ ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lower() == 'true' -#################################### -# OPENTELEMETRY -#################################### +# OpenTelemetry ENABLE_OTEL = os.getenv('ENABLE_OTEL', 'False').lower() == 'true' ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true' diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index 7433f1cd5f..25768d057d 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -43,17 +43,7 @@ from typing_extensions import Self log = logging.getLogger(__name__) -# ── SSL URL normalization (used by sync engine & Alembic migrations) ─ -# -# psycopg2 (sync) needs ``sslmode=`` in the connection string (it does -# not recognise the bare ``ssl=`` key that some ORMs emit). The helpers -# below strip all SSL-related query params, normalise them, and -# reattach them in the canonical libpq form. -# -# The **async** engine now uses psycopg (v3), which speaks libpq -# natively, so it needs no translation at all — the DATABASE_URL is -# passed through as-is. -# ───────────────────────────────────────────────────────────────────── +# Normalize SSL query parameters for sync psycopg2 and Alembic; async psycopg uses the URL unchanged. def _pop_first(params: dict[str, list[str]], key: str) -> str | None: @@ -281,10 +271,8 @@ def _create_async_engine(*args, **kwargs): return create_async_engine(*args, **_json_codec_kwargs(kwargs)) -# ============================================================ -# SYNC ENGINE (used only for: startup migrations, config loading, +# Sync engine (used only for: startup migrations, config loading, # Alembic, peewee migration, health checks) -# ============================================================ # Handle SQLCipher URLs if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'): @@ -443,9 +431,7 @@ def get_session(): get_db = contextmanager(get_session) -# ============================================================ -# ASYNC ENGINE (used for ALL runtime database operations) -# ============================================================ +# Async engine (used for all runtime database operations) # psycopg (v3) speaks libpq natively — the full DATABASE_URL is passed # through as-is. SSL params, ``options``, ``target_session_attrs``, etc. diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index e2b762527e..54441735e8 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -587,11 +587,7 @@ app.state.USER_COUNT = None app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL -######################################## -# -# OPENTELEMETRY -# -######################################## +# OpenTelemetry if ENABLE_OTEL: from open_webui.utils.telemetry.setup import setup as setup_opentelemetry @@ -599,69 +595,34 @@ if ENABLE_OTEL: setup_opentelemetry(app=app, db_engine=engine) -######################################## -# -# OLLAMA -# -######################################## +# Ollama app.state.OLLAMA_MODELS = {} -######################################## -# -# OPENAI -# -######################################## +# OpenAI app.state.OPENAI_MODELS = {} -######################################## -# -# TOOL SERVERS -# -######################################## +# Tool servers app.state.TOOL_SERVERS = [] -######################################## -# -# TERMINAL SERVER -# -######################################## +# Terminal server app.state.TERMINAL_SERVERS = [] -######################################## -# -# DIRECT CONNECTIONS -# -######################################## - - -######################################## -# # SCIM -# -######################################## app.state.ENABLE_SCIM = ENABLE_SCIM app.state.SCIM_TOKEN = SCIM_TOKEN -######################################## -# -# MODELS -# -######################################## +# Models app.state.BASE_MODELS = [] -######################################## -# -# WEBUI -# -######################################## +# WebUI async def initialize_runtime_config(app: FastAPI): @@ -796,25 +757,7 @@ async def initialize_runtime_config(app: FastAPI): ) -######################################## -# -# CODE EXECUTION -# -######################################## - - -######################################## -# -# IMAGES -# -######################################## - - -######################################## -# -# AUDIO -# -######################################## +# Audio app.state.faster_whisper_model = None @@ -822,18 +765,7 @@ app.state.speech_synthesiser = None app.state.speech_speaker_embeddings_dataset = None -######################################## -# -# TASKS -# -######################################## - - -######################################## -# -# WEBUI -# -######################################## +# WebUI app.state.MODELS = MODELS @@ -861,14 +793,8 @@ if ENABLE_COMPRESSION_MIDDLEWARE: app.add_middleware(CompressMiddleware) -# All HTTP middlewares below are pure-ASGI implementations. The previous -# `BaseHTTPMiddleware` / `@app.middleware('http')` versions wrapped the -# downstream app in an anyio task group whose cancel scope cancelled -# in-flight DB calls (and any other awaits) on client disconnect / -# response completion — which surfaced as noisy SQLAlchemy -# `terminate_force_close` tracebacks under aiosqlite and as random -# CancelledError storms across the request path. See -# `open_webui.utils.asgi_middleware` for the rationale. +# Use pure ASGI middleware to avoid BaseHTTPMiddleware cancellation of in-flight DB work. +# See open_webui.utils.asgi_middleware for the disconnect and cleanup rationale. app.add_middleware(AppHTTPMiddleware) @@ -935,11 +861,7 @@ if ENABLE_SCIM: app.include_router(scim.router, prefix='/api/v1/scim/v2', tags=['scim']) -################################## -# # Chat Endpoints -# -################################## @app.get('/api/models') @@ -947,7 +869,7 @@ if ENABLE_SCIM: async def get_models(request: Request, refresh: bool = False, user=Depends(get_verified_user)): all_models = await get_all_models(request, refresh=refresh, user=user) - # Filter out filter pipelines + # Keep filter pipelines off the model menu; they work backstage. models = [ model for model in all_models if not ('pipeline' in model and model['pipeline'].get('type', None) == 'filter') ] @@ -964,7 +886,7 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v info = model.get('info') if isinstance(model.get('info'), dict) else {} meta = info.get('meta') if isinstance(info.get('meta'), dict) else {} - # Remove profile image URL to reduce payload size + # Leave profile images out; this model list is traveling light. meta.pop('profile_image_url', None) if 'tags' in meta: @@ -976,7 +898,7 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v model_order_list = await Config.get('ui.model_order_list') if model_order_list: model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)} - # Sort models by order list priority, with fallback for those not in the list + # Configured models get the front seats; the rest line up by name. models.sort( key=lambda model: ( model_order_dict.get(model.get('id', ''), float('inf')), @@ -1021,7 +943,7 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend break model_id = model_info.base_model_id - # --- Ollama provider --- + # Ollama provider if model_id in ollama_models: ollama_config = await Config.get_many('ollama.base_urls', 'ollama.api_configs') ollama_base_urls = ollama_config.get('ollama.base_urls') or [] @@ -1067,7 +989,7 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend ) return {'status': True} - # --- OpenAI-compatible providers --- + # OpenAI-compatible providers if model_id in openai_models: openai_config = await Config.get_many('openai.api_configs', 'openai.api_base_urls', 'openai.api_keys') openai_api_configs = openai_config.get('openai.api_configs') or {} @@ -1114,9 +1036,7 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend raise HTTPException(status_code=404, detail=f'Model "{model_id}" not found') -################################## # Embeddings -################################## @app.post('/api/embeddings') @@ -1975,11 +1895,7 @@ async def resolve_chat_message_tool_call( app.state.CHAT_COMPLETION_HANDLER = chat_completion -################################## -# # Anthropic Messages API Compatible Endpoint -# -################################## from open_webui.utils.anthropic import ( @@ -2108,13 +2024,13 @@ async def generate_messages( return await passthrough_anthropic_messages(request, form_data, user) passthrough_params = api_config.get('passthrough_params') or [] - # Convert Anthropic payload to OpenAI format + # Translate Anthropic into OpenAI before sending it through the house plumbing. openai_payload = convert_anthropic_to_openai_payload(form_data, passthrough_params) # Route through the existing chat_completion handler response = await chat_completion(request, openai_payload, user) - # Convert response back to Anthropic format + # Translate back to Anthropic; the caller ordered that dialect. if isinstance(response, StreamingResponse): # Streaming response: wrap the generator to convert SSE format return StreamingResponse( @@ -2291,11 +2207,7 @@ async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De return result -################################## -# # Config Endpoints -# -################################## @app.get('/api/config') @@ -2434,7 +2346,7 @@ async def get_app_config(request: Request): }, 'features': { 'slim': USE_SLIM, - # --- Public: required by login/signup page pre-auth --- + # Public: required by login/signup page pre-auth 'auth': WEBUI_AUTH, 'auth_trusted_header': bool(WEBUI_AUTH_TRUSTED_EMAIL_HEADER), 'enable_signup_password_confirmation': ENABLE_SIGNUP_PASSWORD_CONFIRMATION, @@ -2447,7 +2359,7 @@ async def get_app_config(request: Request): if WEBSOCKET_HEARTBEAT_INTERVAL is not None else {} ), - # --- Authenticated: only consumed by logged-in frontend --- + # Authenticated: only consumed by logged-in frontend **( { 'enable_api_keys': config.get('auth.enable_api_keys'), @@ -2776,7 +2688,7 @@ async def get_current_usage(user=Depends(get_verified_user)): raise HTTPException(status_code=500, detail='Internal Server Error') -# --- OAuth Login & Callback --- +# OAuth login & callback try: @@ -2992,9 +2904,7 @@ async def oauth_login_callback( return await oauth_manager.handle_callback(request, provider, response, db=db) -############################ # OIDC Back-Channel Logout -############################ @app.post('/oauth/backchannel-logout') @@ -3155,7 +3065,7 @@ async def check_db_health(): return {'status': True} -# --- static assets & files --- +# Static assets & files # Windows registry entries can override these with text/plain, which breaks module and wasm loading mimetypes.add_type('text/javascript', '.js') mimetypes.add_type('text/javascript', '.mjs') diff --git a/backend/open_webui/migrations/util.py b/backend/open_webui/migrations/util.py index 1c55faf0e9..05407a4358 100644 --- a/backend/open_webui/migrations/util.py +++ b/backend/open_webui/migrations/util.py @@ -6,7 +6,7 @@ from alembic import op # noqa: E402 — alembic runtime context from sqlalchemy import inspect # metadata inspection -# --- database helper functions --- +# Database helper functions def get_existing_tables() -> set[str]: """Return table names already present in the database.""" conn = op.get_bind() diff --git a/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py index ff6fe65118..8b571dccea 100644 --- a/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py +++ b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py @@ -22,7 +22,7 @@ def upgrade() -> None: inspector = sa.inspect(op.get_bind()) existing_tables = set(inspector.get_table_names()) - # ── Create oauth_session table (idempotent) ─────────────────────── + # Create oauth_session table (idempotent) if 'oauth_session' not in existing_tables: op.create_table( 'oauth_session', diff --git a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py index ca75fff7d1..e55f591890 100644 --- a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py +++ b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py @@ -178,7 +178,7 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [ ] -# --- migration execution --- +# Migration execution def upgrade() -> None: # deploy initial schema tables existing_tables = set(get_existing_tables()) for name, columns, constraints in _INITIAL_TABLES: @@ -186,7 +186,7 @@ def upgrade() -> None: # deploy initial schema tables op.create_table(name, *columns, *constraints) -# --- rollback function --- +# Rollback function def downgrade() -> None: # rollback initial schema tables for table_name, _, _ in reversed(_INITIAL_TABLES): op.drop_table(table_name) diff --git a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py index 65dcf6f5f9..1f3d6685ba 100644 --- a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py +++ b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py @@ -22,7 +22,7 @@ def upgrade(): existing_tables = set(inspector.get_table_names()) if 'feedback' not in existing_tables: - # ### Create feedback table ### + # Create feedback table op.create_table( 'feedback', sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type) @@ -42,5 +42,5 @@ def upgrade(): def downgrade(): - # ### Drop feedback table ### + # Drop feedback table op.drop_table('feedback') diff --git a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py index 1de7af315f..fbb9f8f2c9 100644 --- a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py +++ b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py @@ -20,7 +20,7 @@ down_revision: Union[str, None] = '2f1211949ecc' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None -# ── Ad-hoc table references for Core DML ───────────────────────────────── +# Ad-hoc table references for Core DML # These are lightweight table() / column() references used only inside this # migration for SELECT / UPDATE / INSERT — they do NOT create or alter # anything on disk. @@ -143,7 +143,7 @@ def upgrade() -> None: existing_tables = set(inspector.get_table_names()) user_columns = {c['name'] for c in inspector.get_columns('user')} - # ── Add new columns (idempotent) ────────────────────────────────── + # Add new columns (idempotent) for col_name, col_type in [ ('profile_banner_image_url', sa.Text()), ('timezone', sa.String()), @@ -164,7 +164,7 @@ def upgrade() -> None: if isinstance(user_col_types.get('settings'), sa.Text): _convert_column_to_json('user', 'settings') - # ── Create api_key table (idempotent) ───────────────────────────── + # Create api_key table (idempotent) if 'api_key' not in existing_tables: op.create_table( 'api_key', @@ -178,7 +178,7 @@ def upgrade() -> None: sa.Column('updated_at', sa.BigInteger(), nullable=False), ) - # ── Migrate oauth_sub → oauth JSON (only if old column still exists) + # Migrate oauth_sub → OAuth JSON (only if old column still exists) if 'oauth_sub' in user_columns: rows = conn.execute(sa.select(_user.c.id, _user.c.oauth_sub).where(_user.c.oauth_sub.is_not(None))).fetchall() @@ -187,7 +187,7 @@ def upgrade() -> None: provider, sub = oauth_sub.split('@', 1) if '@' in oauth_sub else ('oidc', oauth_sub) conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth={provider: {'sub': sub}})) - # ── Migrate api_key column → api_key table (only if old column still exists) + # Migrate api_key column → api_key table (only if old column still exists) if 'api_key' in user_columns: rows = conn.execute(sa.select(_user.c.id, _user.c.api_key).where(_user.c.api_key.is_not(None))).fetchall() now = int(time.time()) @@ -204,7 +204,7 @@ def upgrade() -> None: ) ) - # ── Drop legacy columns (idempotent) ────────────────────────────── + # Drop legacy columns (idempotent) cols_to_drop = {'api_key', 'oauth_sub'} & user_columns if cols_to_drop: if conn.dialect.name == 'sqlite': @@ -235,7 +235,7 @@ def downgrade() -> None: op.drop_column('user', 'oauth') - # --- Restore api_key field --- + # Restore api_key field op.add_column('user', sa.Column('api_key', sa.String(), nullable=True)) keys = conn.execute(sa.select(_api_key.c.user_id, _api_key.c.key)).fetchall() diff --git a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py index 75eada2d3f..7d42a649b5 100644 --- a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py +++ b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py @@ -23,7 +23,7 @@ def upgrade(): inspector = sa.inspect(conn) file_cols = {c['name'] for c in inspector.get_columns('file')} - # ### commands auto generated by Alembic - please adjust! ### + # Commands auto generated by alembic - please adjust! if 'hash' not in file_cols: op.add_column('file', sa.Column('hash', sa.Text(), nullable=True)) if 'data' not in file_cols: @@ -33,7 +33,7 @@ def upgrade(): def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### + # Commands auto generated by alembic - please adjust! op.drop_column('file', 'updated_at') op.drop_column('file', 'data') op.drop_column('file', 'hash') diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index 53e9a81a69..36bf633d03 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -17,9 +17,7 @@ PRINCIPAL_TYPE_USER = 'user' WILDCARD_PRINCIPAL_ID = '*' -#################### # AccessGrant DB Schema -#################### class AccessGrant(Base): @@ -75,9 +73,7 @@ class AccessGrantResponse(BaseModel): ) -#################### # Conversion utilities -#################### def access_control_to_grants( @@ -318,9 +314,7 @@ def grants_to_access_control(grants: list) -> Optional[dict]: return result -#################### # Table Operations -#################### class AccessGrantsTable: diff --git a/backend/open_webui/models/apps.py b/backend/open_webui/models/apps.py index 2501d4de46..0433e48ce4 100644 --- a/backend/open_webui/models/apps.py +++ b/backend/open_webui/models/apps.py @@ -205,9 +205,7 @@ class AppInteractionForm(StrictModel): action_id: str = Field(min_length=1, max_length=100) -#################### # Forms -#################### class AppManifestForm(StrictModel): @@ -244,9 +242,7 @@ class AppConnectionForm(StrictModel): api_key: SecretStr | None = Field(default=None, min_length=1, max_length=4096) -#################### # Responses -#################### class AppResponse(BaseModel): diff --git a/backend/open_webui/models/auths.py b/backend/open_webui/models/auths.py index f5350cc737..0ee67d73e4 100644 --- a/backend/open_webui/models/auths.py +++ b/backend/open_webui/models/auths.py @@ -207,7 +207,7 @@ class AddUserForm(SignupForm): role: str | None = 'pending' -# --- data-access layer --- +# Data-access layer class AuthsTable: @@ -393,7 +393,7 @@ class AuthsTable: await session.commit() await Users.update_user_by_id(user_id, {'email': email}, db=session) return True - # --- password modification --- + # Password modification async def update_user_password_by_id( self, diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py index bba4b1d028..7f5707d78a 100644 --- a/backend/open_webui/models/automations.py +++ b/backend/open_webui/models/automations.py @@ -13,9 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### # Automation DB Schema -#################### class Automation(Base): @@ -56,9 +54,7 @@ class AutomationRun(Base): ) -#################### # Pydantic Models -#################### class AutomationTerminalConfig(BaseModel): @@ -125,9 +121,7 @@ class AutomationListResponse(BaseModel): total: int -#################### -# AutomationTable -#################### +# Automation Table class AutomationTable: @@ -354,9 +348,7 @@ class AutomationTable: return [AutomationModel.model_validate(r) for r in claimed] -#################### -# AutomationRunTable -#################### +# Automation Run Table class AutomationRunTable: diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py index 42fbe47cea..7bcd393ebc 100644 --- a/backend/open_webui/models/calendar.py +++ b/backend/open_webui/models/calendar.py @@ -31,9 +31,7 @@ log = logging.getLogger(__name__) MIN_CALENDAR_RRULE_INTERVAL_SECONDS = 24 * 60 * 60 -#################### # Calendar DB Schema -#################### class Calendar(Base): @@ -98,9 +96,7 @@ class CalendarEventAttendee(Base): ) -#################### # Pydantic Models -#################### class CalendarModel(BaseModel): @@ -159,9 +155,7 @@ class CalendarEventAttendeeModel(BaseModel): updated_at: int -#################### # Forms -#################### class CalendarForm(BaseModel): @@ -228,9 +222,7 @@ class RSVPForm(BaseModel): status: str # 'accepted' | 'declined' | 'tentative' | 'pending' -#################### # Response Models -#################### class CalendarEventUserResponse(CalendarEventModel): @@ -242,9 +234,7 @@ class CalendarEventListResponse(BaseModel): total: int -#################### # Table Operations -#################### class CalendarTable: diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index 140562f1c2..1f0d15f47b 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -34,9 +34,7 @@ from sqlalchemy import ( from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.asyncio import AsyncSession -#################### # Channel DB Schema -#################### class Channel(Base): @@ -226,9 +224,7 @@ class ChannelWebhookModel(BaseModel): updated_at: int # timestamp in epoch (time_ns) -#################### # Forms -#################### class ChannelResponse(ChannelModel): @@ -794,7 +790,7 @@ class ChannelTable: allowed_channels = [] for channel in channels: - # --- Case A: group or dm => user must be an active member --- + # Case a: group or dm => user must be an active member if channel.type in ['group', 'dm']: result = await db.execute( select(ChannelMember) @@ -810,7 +806,7 @@ class ChannelTable: allowed_channels.append(await self._to_channel_model(channel, db=db)) continue - # --- Case B: standard channel => rely on ACL permissions --- + # Case b: standard channel => rely on acl permissions stmt = select(Channel).filter(Channel.id == channel.id) stmt = self._has_permission( @@ -972,9 +968,7 @@ class ChannelTable: await emit_changes(recipients) return True - #################### # Webhook Methods - #################### async def insert_webhook( self, diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index d7ed13223c..58b1fed141 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -32,9 +32,7 @@ from sqlalchemy import ( ) from sqlalchemy.ext.asyncio import AsyncSession -#################### # Helpers -#################### def _normalize_timestamp(timestamp: Optional[int]) -> float: @@ -43,11 +41,11 @@ def _normalize_timestamp(timestamp: Optional[int]) -> float: if timestamp is None: return now - # Convert milliseconds to seconds if needed + # Treat values above the seconds-range cutoff as milliseconds. if timestamp > 10_000_000_000: timestamp = timestamp / 1000 - # Validate: must be after 2020 and not in the future (with 1 day tolerance) + # Accept timestamps from 2020-01-01 through one day ahead of now, inclusive. min_valid = 1577836800 # 2020-01-01 00:00:00 UTC max_valid = now + 86400 # 1 day in the future (clock skew tolerance) @@ -157,9 +155,7 @@ def _extract_tool_names(value: Any) -> list[str]: return names -#################### # ChatMessage DB Schema -#################### class ChatMessage(Base): @@ -210,9 +206,7 @@ class ChatMessage(Base): ) -#################### # Pydantic Models -#################### class ChatMessageModel(BaseModel): @@ -239,9 +233,7 @@ class ChatMessageModel(BaseModel): updated_at: int -#################### # Table Operations -#################### class ChatMessageTable: diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index b34704a7c9..51ae9933bd 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -297,9 +297,7 @@ class ChatFileModel(BaseModel): model_config = ConfigDict(from_attributes=True) -#################### # Forms -#################### class ChatForm(BaseModel): diff --git a/backend/open_webui/models/config.py b/backend/open_webui/models/config.py index f298b889d6..f6de0a488b 100644 --- a/backend/open_webui/models/config.py +++ b/backend/open_webui/models/config.py @@ -98,7 +98,7 @@ def _json_value(value: Any) -> Any: return jsonable_encoder(value) -# ── Model ──────────────────────────────────────────────────────────────────── +# Model class Config(Base): @@ -114,7 +114,7 @@ class Config(Base): PERSISTENT_ENABLED: ClassVar[bool] = True OAUTH_PERSISTENT_ENABLED: ClassVar[bool] = False - # ── Class methods ──────────────────────────────────────── + # Class methods @classmethod def configure( diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index ca1fe39b38..6bdeb7c06a 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -12,9 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### # Feedback DB Schema -#################### class Feedback(Base): @@ -44,9 +42,7 @@ class FeedbackModel(BaseModel): model_config = ConfigDict(from_attributes=True) -#################### # Forms -#################### class FeedbackResponse(BaseModel): diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index d1f61c7cc7..f2eaab3b1a 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -48,7 +48,7 @@ class FileModel(BaseModel): updated_at: int | None # timestamp in epoch -# --- metadata structures --- +# Metadata structures class FileMeta(BaseModel): name: str | None = None content_type: str | None = None diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index 828119440d..7973c9efed 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -12,11 +12,9 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### # Folder DB Schema # Let every room in this house shelter someone who needs it, # and let no chamber stand empty while there is want. -#################### class Folder(Base): @@ -78,9 +76,7 @@ class SharedFolderResponse(BaseModel): updated_at: int -#################### # Forms -#################### class FolderForm(BaseModel): diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index a06f2bac17..522def658a 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -59,7 +59,7 @@ class FunctionModel(BaseModel): model_config = ConfigDict(from_attributes=True) # allows ORM model binding -# --- form / schema definitions --- +# Form / schema definitions class FunctionWithValvesModel(BaseModel): version_id: str | None = None id: str @@ -77,9 +77,7 @@ class FunctionWithValvesModel(BaseModel): model_config = ConfigDict(from_attributes=True) -#################### # Forms -#################### class FunctionResponse(BaseModel): diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index 7d477f1836..2552b3c6e7 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -30,11 +30,7 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### # UserGroup DB Schema -# Let none who belong to this house be turned away, -# and let the covenant hold for every member. -#################### class Group(Base): @@ -102,9 +98,7 @@ class GroupMemberModel(BaseModel): updated_at: Optional[int] = None # timestamp in epoch -#################### # Forms -#################### class GroupResponse(GroupModel): diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index b2c6c0f5ec..de3297d6af 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -41,11 +41,9 @@ log = logging.getLogger(__name__) # Columns the knowledge base list may be ordered by; anything else falls back to the default. KNOWLEDGE_SORTABLE_FIELDS = {'name', 'created_at', 'updated_at'} -#################### # Knowledge DB Schema # Let what was gathered here outlast the one who gathered it, # and still teach when the builder is gone. -#################### class Knowledge(Base): @@ -149,9 +147,7 @@ class KnowledgeDirectoryForm(BaseModel): parent_id: Optional[str] = None -#################### # Forms -#################### class KnowledgeUserModel(KnowledgeModel): user: Optional[UserResponse] = None file_count: int | None = None @@ -554,12 +550,12 @@ class KnowledgeTable: .filter(KnowledgeFile.knowledge_id == knowledge_id) ) - # Filter by directory_id (None = root level) + # Omit directory_id to include all directories; supply it to restrict the listing. directory_id = filter.get('directory_id') if filter else None if directory_id: stmt = stmt.filter(KnowledgeFile.directory_id == directory_id) elif filter and 'directory_id' in filter: - # Explicit None = root level only + # A supplied but falsy directory_id selects root-level files only. stmt = stmt.filter(KnowledgeFile.directory_id.is_(None)) # Default sort: updated_at descending @@ -885,7 +881,7 @@ class KnowledgeTable: except Exception: return False - # ── Directory CRUD ──────────────────────────────────────────────── + # Directory CRUD async def create_directory( self, diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index 8568420f71..0ba78c2eb2 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -12,9 +12,7 @@ from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, and_, de from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql import exists -#################### # Message DB Schema -#################### class MessageReaction(Base): @@ -89,9 +87,7 @@ class MessageModel(BaseModel): return str(self.updated_at) -#################### # Forms -#################### class MessageForm(BaseModel): diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 0384d5981c..fa9525d7e0 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -67,7 +67,7 @@ def strip_extracted_content_from_model_knowledge(knowledge: Any) -> Any: return sanitized -# --- Models DB Schema --- +# Models DB schema ModelControlKey = Annotated[str, Field(pattern=re.compile(r'^(?!(?:constructor|prototype)\Z)[a-zA-Z][a-zA-Z0-9_-]*\Z'))] diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index e7c1fcae78..53c87d2e8e 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -386,9 +386,7 @@ def matching_page(data: dict, query: str = '') -> dict: return pages[0] -#################### # Note DB Schema -#################### class Note(Base): @@ -462,9 +460,7 @@ class PinnedNote(Base): created_at = Column(BigInteger, nullable=False) -#################### # Forms -#################### class NoteForm(BaseModel): diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index 25a50ced55..13d5d773a9 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -15,9 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# DB MODEL -#################### +# DB model class OAuthSession(Base): @@ -51,9 +49,7 @@ class OAuthSessionModel(BaseModel): model_config = ConfigDict(from_attributes=True) -#################### # Forms -#################### class OAuthSessionResponse(BaseModel): diff --git a/backend/open_webui/models/prompt_history.py b/backend/open_webui/models/prompt_history.py index 78c3330b4a..60dcb74145 100644 --- a/backend/open_webui/models/prompt_history.py +++ b/backend/open_webui/models/prompt_history.py @@ -11,9 +11,7 @@ from pydantic import BaseModel, ConfigDict from sqlalchemy import JSON, BigInteger, Column, Index, Text, delete, func, select from sqlalchemy.ext.asyncio import AsyncSession -#################### # PromptHistory DB Schema -#################### class PromptHistory(Base): diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index c486e55e97..d7252d665e 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -59,9 +59,8 @@ class PromptModel(BaseModel): model_config = ConfigDict(from_attributes=True) # allows ORM model binding -# --- form / schema definitions --- +# Form / schema definitions # Forms -#################### class PromptUserResponse(PromptModel): @@ -196,7 +195,7 @@ class PromptsTable: if match is None: return return await self._to_prompt_model(match, db=session) - # --- context manager always returns above --- + # Context manager always returns above return async def get_prompts(self, db: AsyncSession | None = None) -> list[PromptUserResponse]: diff --git a/backend/open_webui/models/shared_chats.py b/backend/open_webui/models/shared_chats.py index 8959d6c771..cd88eef241 100644 --- a/backend/open_webui/models/shared_chats.py +++ b/backend/open_webui/models/shared_chats.py @@ -17,9 +17,7 @@ class ShareChatForm(BaseModel): share_mode: ChatShareMode = None -#################### # SharedChat DB Schema -#################### class SharedChat(Base): @@ -59,9 +57,7 @@ class SharedChatResponse(BaseModel): created_at: int -#################### # Table Operations -#################### class SharedChatsTable: diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py index 3dbc0f5514..9a3f17952f 100644 --- a/backend/open_webui/models/skills.py +++ b/backend/open_webui/models/skills.py @@ -18,9 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### # Skills DB Schema -#################### class Skill(Base): @@ -63,9 +61,7 @@ class SkillModel(BaseModel): model_config = ConfigDict(from_attributes=True) -#################### # Forms -#################### class SkillUserModel(SkillModel): diff --git a/backend/open_webui/models/tags.py b/backend/open_webui/models/tags.py index 9f24d274bb..2db4d0c8af 100644 --- a/backend/open_webui/models/tags.py +++ b/backend/open_webui/models/tags.py @@ -39,9 +39,8 @@ class TagModel(BaseModel): model_config = ConfigDict(from_attributes=True) # allows ORM model binding -# --- tag schema forms --- +# Tag schema forms # Forms -#################### class TagChatIdForm(BaseModel): diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index 8b8cb0f5c7..289420c8d9 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -66,9 +66,8 @@ class ToolModel(BaseModel): model_config = ConfigDict(from_attributes=True) # enables ORM mapping -# --- tool request forms --- +# Tool request forms # Forms -#################### class ToolUserModel(ToolModel): diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index ee01cd7444..28c0f49415 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -35,11 +35,9 @@ from sqlalchemy import ( ) from sqlalchemy.ext.asyncio import AsyncSession -#################### # User DB Schema -# Hallowed be the columns defined here, for they hold the -# daily bread of every session. Let none go hungry. -#################### +# Let every name entrusted to these records find its place, +# and let no one be forgotten among the many. class InterfaceTitleSettings(BaseModel): @@ -217,7 +215,7 @@ class UserModel(BaseModel): ) # validation schema logic - # --- model validators --- + # Model validators @model_validator(mode='after') def _ensure_profile_image(self) -> 'UserModel': """Assign a generated avatar when no profile image is provided.""" @@ -262,9 +260,7 @@ class ApiKeyModel(BaseModel): model_config = ConfigDict(from_attributes=True) -#################### # Forms -#################### class UpdateProfileForm(BaseModel): @@ -410,7 +406,7 @@ class UsersTable: return user if result else None # database read methods - # --- read / lookup operations --- + # Read / lookup operations async def get_user_by_id( self, id: str, @@ -448,10 +444,10 @@ class UsersTable: if match is None: return return UserModel.model_validate(match) - # --- context manager above always returns --- + # Context manager above always returns return - # --- oauth & integrations --- + # OAuth & integrations async def get_user_by_oauth_sub( self, provider: str, diff --git a/backend/open_webui/retrieval/loaders/mistral.py b/backend/open_webui/retrieval/loaders/mistral.py index a5dbebfff1..efa973f8e8 100644 --- a/backend/open_webui/retrieval/loaders/mistral.py +++ b/backend/open_webui/retrieval/loaders/mistral.py @@ -65,7 +65,7 @@ class MistralLoader: self.params = params or {} self.user = user - # PERFORMANCE OPTIMIZATION: Differentiated timeouts for different operations + # Use separate timeouts for different operations. # This prevents long-running OCR operations from affecting quick operations # and improves user experience by failing fast on operations that should be quick self.upload_timeout = min(timeout, 120) # Cap upload at 2 minutes - prevents hanging on large files @@ -73,12 +73,12 @@ class MistralLoader: self.ocr_timeout = timeout # OCR can take the full timeout - this is the heavy operation self.cleanup_timeout = 30 # Cleanup should be quick - don't hang on file deletion - # PERFORMANCE OPTIMIZATION: Pre-compute file info to avoid repeated filesystem calls + # Precompute file info to avoid repeated filesystem calls. # This avoids multiple os.path.basename() and os.path.getsize() calls during processing self.file_name = os.path.basename(file_path) self.file_size = os.path.getsize(file_path) - # ENHANCEMENT: Added User-Agent for better API tracking and debugging + # Include User-Agent for API tracking and debugging. self.headers = { 'Authorization': f'Bearer {self.api_key}', 'User-Agent': 'OpenWebUI-MistralLoader/2.0', # Helps API provider track usage @@ -161,7 +161,7 @@ class MistralLoader: if attempt == self.max_retries - 1 or not self._is_retryable_error(e): raise - # PERFORMANCE OPTIMIZATION: Exponential backoff with cap + # Cap exponential backoff between retries. # Prevents overwhelming the server while ensuring reasonable retry delays wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds log.warning( @@ -182,7 +182,7 @@ class MistralLoader: url = f'{self.base_url}/files' def upload_request(): - # MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime + # Limit the file handle's lifetime with a context manager. # This ensures the file is closed immediately after reading, reducing memory usage with open(self.file_path, 'rb') as f: files = {'file': (self.file_name, f, 'application/pdf')} diff --git a/backend/open_webui/retrieval/models/colbert.py b/backend/open_webui/retrieval/models/colbert.py index b388974701..98d63a69d7 100644 --- a/backend/open_webui/retrieval/models/colbert.py +++ b/backend/open_webui/retrieval/models/colbert.py @@ -34,10 +34,7 @@ class ColBERT(BaseReranker): DOCKER = kwargs.get('env') == 'docker' if DOCKER: - # This is a workaround for the issue with the docker container - # where the torch extension is not loaded properly - # and the following error is thrown: - # /root/.cache/torch_extensions/py312_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory + # Remove the stale build lock so Docker can load the segmented_maxsim_cpp extension. lock_file = '/root/.cache/torch_extensions/py312_cpu/segmented_maxsim_cpp/lock' if os.path.exists(lock_file): diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 24963eafcd..a6ea444e82 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -243,12 +243,7 @@ def _get_content_from_url_sync(request, url: str, config: dict): # Validate URL before making any request (blocks private IPs, non-HTTP, filter list) validate_url(url) - # YouTube URLs (including youtu.be short links) should go straight to - # YoutubeLoader, which uses youtube-transcript-api and never needs the - # HTTP response body. Probing the URL first is harmful for short URLs: - # youtu.be returns a 303 redirect with Content-Type: application/binary - # when allow_redirects=False, causing the binary-content path to run - # and produce empty docs → HTTP 400. + # Send YouTube URLs directly to YoutubeLoader; probing youtu.be can misclassify its 303 as binary. if is_youtube_url(url): loader = get_loader(request, url, config) docs = loader.load() @@ -814,11 +809,7 @@ async def query_collection_with_hybrid_search( if native_task_results and all(result is not None for result in native_task_results): return merge_and_sort_query_results(native_task_results, k=k) - # Fetch every collection's contents once up front so the - # per-query/per-document loop below can reuse them. Each fetch - # offloads to a worker thread, so run them concurrently with - # `asyncio.gather` instead of awaiting them serially — otherwise - # latency scales linearly with `len(collection_names)`. + # Fetch collections concurrently once, then reuse the results across queries and documents. log.debug( 'query_collection_with_hybrid_search: prefetching %d collections', len(collection_names), diff --git a/backend/open_webui/retrieval/vector/dbs/valkey.py b/backend/open_webui/retrieval/vector/dbs/valkey.py index c315a0a5fb..b96507c3ec 100644 --- a/backend/open_webui/retrieval/vector/dbs/valkey.py +++ b/backend/open_webui/retrieval/vector/dbs/valkey.py @@ -237,7 +237,7 @@ class ValkeyClient(VectorDBBase): except Exception: pass - # ----- version checks ---------------------------------------------------- + # Version checks @staticmethod def _parse_semver(version_str: str) -> tuple[int, int, int] | None: diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py index e455bb596f..6d0f2dd15f 100644 --- a/backend/open_webui/routers/analytics.py +++ b/backend/open_webui/routers/analytics.py @@ -20,9 +20,7 @@ log = logging.getLogger(__name__) router = APIRouter() -#################### # Response Models -#################### class ModelAnalyticsEntry(BaseModel): @@ -51,9 +49,7 @@ class UserAnalyticsResponse(BaseModel): total: int -#################### # Endpoints -#################### @router.get('/models', response_model=ModelAnalyticsResponse) @@ -283,9 +279,7 @@ async def get_token_usage( ) -#################### # Model Chats Browser -#################### class ModelChatEntry(BaseModel): @@ -344,9 +338,7 @@ async def get_model_chats( ) -#################### # Model Overview -#################### class HistoryEntry(BaseModel): @@ -446,9 +438,7 @@ async def get_model_overview( return ModelOverviewResponse(history=history, tags=tags) -#################### # Session Explorer -#################### def require_session_access(user=Depends(get_admin_user)): diff --git a/backend/open_webui/routers/apps.py b/backend/open_webui/routers/apps.py index beb2bb0f82..528b7dd261 100644 --- a/backend/open_webui/routers/apps.py +++ b/backend/open_webui/routers/apps.py @@ -38,9 +38,7 @@ from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter() -############################ -# GetApps -############################ +# Get Apps @router.get('', response_model=list[AppResponse]) @@ -57,9 +55,7 @@ async def get_apps(user=Depends(get_verified_user), db: AsyncSession = Depends(g ] -############################ -# PreviewAppManifest -############################ +# Preview App Manifest @router.post('/manifest/preview', response_model=Manifest) @@ -96,9 +92,7 @@ async def preview_app_manifest( ) from None -############################ -# CreateNewApp -############################ +# Create New App @router.post('', response_model=AppResponse) @@ -140,9 +134,7 @@ async def create_new_app( return get_app_response(app) -############################ -# GetAppDetailsById -############################ +# Get App Details By Id @router.get('/{app_id}', response_model=AppAccessResponse) @@ -164,9 +156,7 @@ async def get_app_details_by_id( return result -############################ -# UpdateAppById -############################ +# Update App By Id @router.patch('/{app_id}', response_model=AppResponse) @@ -207,9 +197,7 @@ async def update_app_by_id( return get_app_response(app) -############################ -# ConnectAppById -############################ +# Connect App By Id @router.post('/{app_id}/connect', response_model=AppResponse) @@ -245,9 +233,7 @@ async def connect_app_by_id( return {**get_app_response(app), 'connection_status': 'connected'} -############################ -# UninstallAppById -############################ +# Uninstall App By Id @router.delete('/{app_id}', response_model=AppResponse) @@ -257,9 +243,7 @@ async def uninstall_app_by_id(app_id: str, user=Depends(get_admin_user), db: Asy return get_app_response(app) -############################ -# CreateAppInteraction -############################ +# Create App Interaction @router.post('/{app_id}/interactions') diff --git a/backend/open_webui/routers/audio/__init__.py b/backend/open_webui/routers/audio/__init__.py index fda9e11854..8f86d77e5f 100644 --- a/backend/open_webui/routers/audio/__init__.py +++ b/backend/open_webui/routers/audio/__init__.py @@ -72,7 +72,7 @@ log = logging.getLogger(__name__) router = APIRouter() router.include_router(realtime.router) -# --- Constants --- +# Constants MAX_FILE_SIZE_MB: int = 20 MAX_FILE_SIZE: int = MAX_FILE_SIZE_MB * 1024 * 1024 diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 596a524268..a8626479ea 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -90,8 +90,8 @@ router = APIRouter(route_class=MfaRoute) log = logging.getLogger(__name__) -# Forgive us our failed attempts, as we forgive those -# who exceed their allotted rate against this gate. +# Let patience meet those who stumble at the gate, +# and let those within rest safely through the night. signin_rate_limiter = RateLimiter(limit=5 * 3, window=60 * 3) # Best-effort throttle only: there is no caller identity before the provider answers, # and deployments may derive request.client from proxy headers. @@ -242,9 +242,7 @@ async def get_session_user( return response_data -############################ # Update Profile -############################ @router.post('/update/profile', response_model=UserProfileImageResponse) @@ -275,9 +273,7 @@ async def update_profile( raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) -############################ # Update Timezone -############################ class UpdateTimezoneForm(BaseModel): @@ -309,9 +305,7 @@ async def update_timezone( raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) -############################ # Update Password -############################ @router.post('/update/password', response_model=bool) @@ -406,9 +400,7 @@ def extract_group_cn_from_dn(group_dn: str) -> str | None: return None -############################ # LDAP Authentication -############################ @router.post('/ldap', response_model=SigninResult) async def ldap_auth( request: Request, @@ -657,9 +649,7 @@ async def ldap_auth( raise HTTPException(400, detail='LDAP authentication failed.') -############################ -# SignIn -############################ +# Sign In @router.post('/signin', response_model=SigninResult) @@ -790,9 +780,7 @@ async def signin( raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) -############################ -# SignUp -############################ +# Sign Up async def signup_handler( @@ -1016,9 +1004,7 @@ async def signout(request: Request, response: Response, db: AsyncSession = Depen return JSONResponse(status_code=200, content={'status': True}, headers=response.headers) -############################ # OAuth Session Management -############################ @router.delete('/oauth/sessions/{provider:path}', response_model=bool) @@ -1050,9 +1036,7 @@ async def delete_oauth_session_by_provider( return True -############################ -# AddUser -############################ +# Add User @router.post('/add', response_model=AddUserResponse, response_model_exclude_none=True) @@ -1125,9 +1109,7 @@ async def add_user( raise HTTPException(500, detail='An internal error occurred while adding the user.') -############################ -# GetAdminDetails -############################ +# Get Admin Details @router.get('/admin/details') @@ -1158,9 +1140,7 @@ async def get_admin_details( raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) -############################ -# ToggleSignUp -############################ +# Toggle Sign Up @router.get('/admin/config') @@ -1348,9 +1328,7 @@ async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=D return {'ENABLE_LDAP': await Config.get('ldap.enable')} -############################ # API Key -############################ class OAuthConfigForm(BaseModel): @@ -1568,9 +1546,7 @@ async def get_api_key(request: Request, user=Depends(get_current_user), db: Asyn raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) -############################ # Token Exchange -############################ class TokenExchangeForm(BaseModel): diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py index a61490b0e0..509f5d73af 100644 --- a/backend/open_webui/routers/automations.py +++ b/backend/open_webui/routers/automations.py @@ -37,9 +37,7 @@ router = APIRouter() PAGE_ITEM_COUNT = 30 -############################ # Helpers -############################ async def check_automations_permission(request, user): @@ -154,9 +152,7 @@ async def enrich_automation(automation: AutomationModel, db: AsyncSession, tz: s ) -############################ # GetAutomationItems (paginated) -############################ @router.get('/list') @@ -200,9 +196,7 @@ async def get_automation_items( } -############################ -# CreateNewAutomation -############################ +# Create New Automation @router.post('/create', response_model=AutomationResponse) @@ -238,9 +232,7 @@ async def create_new_automation( return response -############################ -# GetAutomationById -############################ +# Get Automation By Id @router.get('/{id}', response_model=AutomationResponse) @@ -256,9 +248,7 @@ async def get_automation_by_id( return await enrich_automation(automation, db, tz=user.timezone) -############################ -# UpdateAutomationById -############################ +# Update Automation By Id @router.post('/{id}/update', response_model=AutomationResponse) @@ -298,9 +288,7 @@ async def update_automation_by_id( return response -############################ -# ToggleAutomationById -############################ +# Toggle Automation By Id @router.post('/{id}/toggle', response_model=AutomationResponse) @@ -326,9 +314,7 @@ async def toggle_automation_by_id( return response -############################ -# RunAutomationById -############################ +# Run Automation By Id @router.post('/{id}/run') @@ -353,9 +339,7 @@ async def run_automation_by_id( return await enrich_automation(automation, db, tz=user.timezone) -############################ -# DeleteAutomationById -############################ +# Delete Automation By Id @router.delete('/{id}/delete') @@ -381,9 +365,7 @@ async def delete_automation_by_id( return result -############################ -# GetAutomationRuns -############################ +# Get Automation Runs @router.get('/{id}/runs', response_model=list[AutomationRunModel]) diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index e4cf3ebb0d..e2cf485c58 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -81,9 +81,7 @@ async def _check_calendar_access(calendar_id: str, user: UserModel, permission: raise HTTPException(status_code=403, detail='Access denied') -#################### # Calendar CRUD (static paths first) -#################### @router.get('/', response_model=list[CalendarModel]) @@ -137,9 +135,7 @@ async def create_calendar(request: Request, form_data: CalendarForm, user: UserM return calendar -#################### # Event CRUD (before /{calendar_id} to avoid route conflicts) -#################### @router.get('/events') @@ -399,9 +395,7 @@ async def rsvp_event( return {'status': True, 'rsvp': result.status} -#################### # Calendar by ID (dynamic path — MUST come after /events* routes) -#################### @router.get('/{calendar_id}', response_model=CalendarModel) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index b2bb710570..b323027d7b 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -149,11 +149,9 @@ async def get_channel_member_user_ids( return list(dict.fromkeys([*user_ids, channel.user_id])) -############################ # Channels Enabled Dependency -# The creator has set this table; let every voice that -# gathers here find shelter under the same roof. -############################ +# Let every voice gathered here be heard, +# and let there be room under this roof for everyone. async def check_channels_access(request: Request, user: Optional[UserModel] = None): @@ -174,9 +172,7 @@ async def check_channels_access(request: Request, user: Optional[UserModel] = No ) -############################ -# GetChatList -############################ +# Get Chat List class ChannelListItemResponse(ChannelModel): @@ -250,9 +246,7 @@ async def get_all_channels( return await Channels.get_channels_by_user_id(user.id, db=db) -############################ -# GetDMChannelByUserId -############################ +# Get DM Channel By User Id @router.get('/users/{user_id}', response_model=Optional[ChannelModel]) @@ -308,9 +302,7 @@ async def get_dm_channel_by_user_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# CreateNewChannel -############################ +# Create New Channel @router.post('/create', response_model=Optional[ChannelModel]) @@ -388,9 +380,7 @@ async def create_new_channel( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetChannelById -############################ +# Get Channel By Id class ChannelFullResponse(ChannelResponse): @@ -492,9 +482,7 @@ async def get_channel_by_id( ) -############################ -# GetChannelMembersById -############################ +# Get Channel Members By Id PAGE_ITEM_COUNT = 30 @@ -602,9 +590,7 @@ async def get_channel_members_by_id( } -################################################# -# UpdateIsActiveMemberByIdAndUserId -################################################# +# Update Is Active Member By Id And User Id class UpdateActiveMemberForm(BaseModel): @@ -670,9 +656,7 @@ async def update_is_active_member_by_id_and_user_id( return True -################################################# -# AddMembersById -################################################# +# Add Members By Id class UpdateMembersForm(BaseModel): @@ -725,9 +709,6 @@ async def add_members_by_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -################################################# -# -################################################# class RemoveMembersForm(BaseModel): @@ -773,9 +754,7 @@ async def remove_members_by_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# UpdateChannelById -############################ +# Update Channel By Id @router.post('/{id}/update', response_model=Optional[ChannelModel]) @@ -853,9 +832,7 @@ async def update_channel_access_by_id( return channel -############################ -# DeleteChannelById -############################ +# Delete Channel By Id @router.delete('/{id}/delete', response_model=bool) @@ -891,9 +868,7 @@ async def delete_channel_by_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetChannelMessages -############################ +# Get Channel Messages @router.get('/{channel_id}/apps') @@ -1046,9 +1021,7 @@ async def get_channel_messages( return messages -############################ -# GetPinnedChannelMessages -############################ +# Get Pinned Channel Messages PAGE_ITEM_COUNT_PINNED = 20 @@ -1118,9 +1091,7 @@ async def get_pinned_channel_messages( return messages -############################ -# PostNewMessage -############################ +# Post New Message async def send_notification(request, channel, message, active_user_ids, db=None): @@ -1370,10 +1341,7 @@ async def model_response_handler(request, channel, message, user, db=None): # Resolve model config (same path automations use) from open_webui.utils.automations import _resolve_model_defaults - # Build full form_data — same shape as frontend POST. - # The channel: prefix routes pipeline events to the - # channel emitter in socket/main.py instead of the - # default chat emitter. + # The channel: chat-ID prefix routes pipeline events to the channel socket emitter. form_data = { **await _resolve_model_defaults(request.app, model_id), 'model': model_id, @@ -1390,10 +1358,7 @@ async def model_response_handler(request, channel, message, user, db=None): if files: form_data['files'] = files - # Call the full chat completion pipeline — streaming, - # tools, filters, RAG — everything. The pipeline runs as - # an async task; the channel emitter handles progressive - # message updates via socket events. + # Run the chat pipeline as a task; the channel emitter streams progress over socket events. await request.app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user) except Exception as e: @@ -1532,9 +1497,7 @@ async def post_new_message( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetChannelMessage -############################ +# Get Channel Message @router.get('/{id}/messages/{message_id}', response_model=Optional[MessageResponse]) @@ -1573,9 +1536,7 @@ async def get_channel_message( ) -############################ -# GetChannelMessageData -############################ +# Get Channel Message Data @router.get('/{id}/messages/{message_id}/data', response_model=Optional[dict]) @@ -1608,9 +1569,7 @@ async def get_channel_message_data( return message.data -############################ -# PinChannelMessage -############################ +# Pin Channel Message class PinMessageForm(BaseModel): @@ -1688,9 +1647,7 @@ async def pin_channel_message( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetChannelThreadMessages -############################ +# Get Channel Thread Messages @router.get('/{id}/messages/{message_id}/thread', response_model=list[MessageUserResponse]) @@ -1754,9 +1711,7 @@ async def get_channel_thread_messages( return messages -############################ -# UpdateMessageById -############################ +# Update Message By Id @router.post('/{id}/messages/{message_id}/update', response_model=Optional[MessageModel]) @@ -1829,9 +1784,7 @@ async def update_message_by_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# AddReactionToMessage -############################ +# Add Reaction To Message class ReactionForm(BaseModel): @@ -1907,9 +1860,7 @@ async def add_reaction_to_message( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# RemoveReactionById -############################ +# Remove Reaction By Id @router.post('/{id}/messages/{message_id}/reactions/remove', response_model=bool) @@ -1982,9 +1933,7 @@ async def remove_reaction_by_id_and_user_id_and_name( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# DeleteMessageById -############################ +# Delete Message By Id @router.delete('/{id}/messages/{message_id}/delete', response_model=bool) @@ -2080,9 +2029,7 @@ async def delete_message_by_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ # Webhooks -############################ @router.get('/webhooks/{webhook_id}/profile/image') @@ -2263,9 +2210,7 @@ async def delete_channel_webhook( return deleted -############################ # Public Webhook Endpoint -############################ class WebhookMessageForm(BaseModel): diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 59c769b584..55283e9dbe 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -251,11 +251,9 @@ async def require_chat_import_permission(request: Request, user, db: AsyncSessio ) -############################ -# GetChatList +# Get Chat List # Let the record outlive the session, so that what was # learned here not need to be learned again. -############################ @router.get('/', response_model=list[ChatTitleIdResponse]) @@ -311,10 +309,8 @@ async def mark_chats_read_by_user_id( } -############################ -# GetChatUsageStats +# Get Chat Usage Stats # EXPERIMENTAL: may be removed in future releases -############################ @router.get('/stats/usage', response_model=ChatUsageStatsListResponse) @@ -422,9 +418,7 @@ async def get_session_user_chat_usage_stats( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetChatStatsExport -############################ +# Get Chat Stats Export CHAT_EXPORT_PAGE_ITEM_COUNT = 10 @@ -478,7 +472,7 @@ def _process_chat_for_export(chat) -> ChatStatsExport | None: export_messages[key] = message_stat - # --- Aggregation Logic (copied/adapted from usage stats) --- + # Aggregation logic (copied/adapted from usage stats) role = message.get('role', '') if role == 'user': history_user_messages.append(message) @@ -660,9 +654,7 @@ async def export_chat_stats( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetSingleChatStatsExport -############################ +# Get Single Chat Stats Export @router.get('/stats/export/{chat_id}', response_model=ChatStatsExport | None) @@ -743,9 +735,7 @@ async def delete_all_user_chats( return result -############################ -# GetUserChatList -############################ +# Get User Chat List @router.get('/list/user/{user_id}', response_model=list[ChatTitleIdResponse]) @@ -781,9 +771,7 @@ async def get_user_chat_list_by_user_id( return await add_active_state_to_chat_list(request, chats) -############################ -# CreateNewChat -############################ +# Create New Chat @router.post('/new', response_model=ChatResponse | None) @@ -817,9 +805,7 @@ async def create_new_chat( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# ImportChats -############################ +# Import Chats @router.post('/import', response_model=list[ChatResponse]) @@ -853,9 +839,7 @@ async def import_chats( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# ChatConfig -############################ +# Chat Config @router.get('/config', response_model=ChatConfigForm) @@ -892,9 +876,7 @@ async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user return await get_chat_config_values() -############################ -# GetChats -############################ +# Get Chats @router.get('/search', response_model=list[ChatTitleIdResponse]) @@ -936,9 +918,7 @@ async def get_chat_search_snippet( return snippets.get(id) -############################ -# GetChatsByFolderId -############################ +# Get Chats By Folder Id @router.get('/folder/{folder_id}', response_model=list[ChatResponse]) @@ -986,9 +966,7 @@ async def get_chat_list_by_folder_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetPinnedChats -############################ +# Get Pinned Chats @router.get('/pinned', response_model=list[ChatTitleIdResponse]) @@ -999,9 +977,7 @@ async def get_user_pinned_chats( return await add_active_state_to_chat_list(request, chats) -############################ -# GetChats -############################ +# Get Chats CHAT_EXPORT_BATCH_SIZE = 100 @@ -1045,9 +1021,7 @@ async def get_user_chats(user=Depends(get_verified_user)): ) -############################ -# GetArchivedChats -############################ +# Get Archived Chats @router.get('/all/archived', response_model=list[ChatResponse]) @@ -1058,9 +1032,7 @@ async def get_user_archived_chats(user=Depends(get_verified_user), db: AsyncSess ] -############################ -# GetAllTags -############################ +# Get All Tags @router.get('/all/tags', response_model=list[TagModel]) @@ -1073,9 +1045,7 @@ async def get_all_user_tags(user=Depends(get_verified_user), db: AsyncSession = raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetAllChatsInDB -############################ +# Get All Chats In DB @router.get('/all/db', response_model=list[ChatResponse]) @@ -1085,9 +1055,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSessio return [ChatResponse.model_validate(chat, from_attributes=True) for chat in await Chats.get_chats(db=db)] -############################ -# GetArchivedChats -############################ +# Get Archived Chats @router.get('/archived', response_model=list[ChatTitleIdResponse]) @@ -1124,9 +1092,7 @@ async def get_archived_session_user_chat_list( return await add_active_state_to_chat_list(request, chats) -############################ -# GetArchivedChatsCount -############################ +# Get Archived Chats Count @router.get('/archived/count', response_model=int) @@ -1137,9 +1103,7 @@ async def get_archived_session_user_chat_count( return await Chats.count_archived_chats_by_user_id(user.id, db=db) -############################ -# ArchiveAllChats -############################ +# Archive All Chats @router.post('/archive/all', response_model=bool) @@ -1152,9 +1116,7 @@ async def archive_all_chats( return result -############################ -# UnarchiveAllChats -############################ +# Unarchive All Chats @router.post('/unarchive/all', response_model=bool) @@ -1173,9 +1135,7 @@ async def unarchive_all_chats( return result -############################ -# UnshareAllChats -############################ +# Unshare All Chats @router.delete('/share/all', response_model=bool) @@ -1238,9 +1198,7 @@ async def get_shared_session_user_chat_list( ) -############################ -# GetSharedChatById -############################ +# Get Shared Chat By Id @router.get('/share/{share_id}', response_model=ChatResponse | None) @@ -1288,9 +1246,7 @@ async def get_shared_chat_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) -############################ -# GetChatsByTags -############################ +# Get Chats By Tags class TagForm(BaseModel): @@ -1318,9 +1274,7 @@ async def get_user_chat_list_by_tag_name( return await add_active_state_to_chat_list(request, chats) -############################ -# CompactChat -############################ +# Compact Chat @router.post('/{id}/compact') @@ -1372,9 +1326,7 @@ async def compact_chat_by_id( return result -############################ -# GetChatById -############################ +# Get Chat By Id @router.get('/{id}', response_model=ChatResponse | None) @@ -1407,9 +1359,7 @@ async def get_chat_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) -############################ -# UpdateChatById -############################ +# Update Chat By Id @router.post('/{id}', response_model=ChatResponse | None) @@ -1466,9 +1416,7 @@ async def update_chat_by_id( ) -############################ -# UpdateChatParams -############################ +# Update Chat Params @router.patch('/{id}/params', response_model=dict) @@ -1499,9 +1447,7 @@ async def update_chat_params( return chat.chat['params'] -############################ -# UpdateChatMessageById -############################ +# Update Chat Message By Id class MessageForm(BaseModel): content: str | None = None voice: dict | None = None @@ -1613,9 +1559,7 @@ async def delete_chat_message_by_id( return ChatResponse.model_validate(chat, from_attributes=True) -############################ -# SendChatMessageEventById -############################ +# Send Chat Message Event By Id class EventForm(BaseModel): type: str data: dict @@ -1669,9 +1613,7 @@ async def send_chat_message_event_by_id( return False -############################ -# DeleteChatById -############################ +# Delete Chat By Id @router.delete('/{id}', response_model=bool) @@ -1727,9 +1669,7 @@ async def delete_chat_by_id( return result -############################ -# GetPinnedStatusById -############################ +# Get Pinned Status By Id @router.get('/{id}/pinned', response_model=bool | None) @@ -1743,9 +1683,7 @@ async def get_pinned_status_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# PinChatById -############################ +# Pin Chat By Id @router.post('/{id}/pin', response_model=ChatResponse | None) @@ -1767,9 +1705,7 @@ async def pin_chat_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# CloneChat -############################ +# Clone Chat class CloneForm(BaseModel): @@ -1956,9 +1892,7 @@ async def clone_chat_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# CloneSharedChatById -############################ +# Clone Shared Chat By Id @router.post('/{id}/clone/shared', response_model=ChatResponse | None) @@ -2040,9 +1974,7 @@ async def clone_shared_chat_by_id( ) -############################ -# ArchiveChat -############################ +# Archive Chat @router.post('/{id}/archive', response_model=ChatResponse | None) @@ -2076,7 +2008,7 @@ async def archive_chat_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -# --- Share Chat --- +# Share chat @router.post('/{id}/share', response_model=ChatResponse | None) @@ -2127,7 +2059,7 @@ async def share_chat_by_id( return ChatResponse.model_validate(chat, from_attributes=True) -# --- Delete Shared Chat --- +# Delete shared chat @router.delete('/{id}/share', response_model=bool | None) @@ -2155,9 +2087,7 @@ async def delete_shared_chat_by_id( return True -############################ -# UpdateSharedChatAccessById -############################ +# Update Shared Chat Access By Id class ChatAccessGrantsForm(BaseModel): @@ -2202,9 +2132,7 @@ async def update_shared_chat_access_by_id( return ChatResponse.model_validate(chat, from_attributes=True) -############################ -# GetSharedChatAccessById -############################ +# Get Shared Chat Access By Id @router.get('/shared/{id}/access', response_model=list) @@ -2235,9 +2163,7 @@ async def get_shared_chat_access_by_id( ] -############################ -# UpdateChatFolderIdById -############################ +# Update Chat Folder Id By Id class ChatFolderIdForm(BaseModel): @@ -2301,9 +2227,7 @@ async def update_chat_folder_id_by_id( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetChatTagsById -############################ +# Get Chat Tags By Id @router.get('/{id}/tags', response_model=list[TagModel]) @@ -2321,9 +2245,7 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user), db: Asyn raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) -############################ -# AddChatTagById -############################ +# Add Chat Tag By Id @router.post('/{id}/tags', response_model=list[TagModel]) @@ -2362,9 +2284,7 @@ async def add_tag_by_id_and_tag_name( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# DeleteChatTagById -############################ +# Delete Chat Tag By Id @router.delete('/{id}/tags', response_model=list[TagModel]) diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 1a8585dd6e..eb42f230be 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -86,11 +86,9 @@ def config_updates(data: dict, key_map: dict[str, str]) -> dict: return {key_map[field]: value for field, value in data.items() if field in key_map} -############################ -# ImportConfig -# Thy configuration come, thy settings be done, -# in production as it is in development. -############################ +# Import Config +# Let what we prepare in quiet hold when the world grows loud, +# and let those who depend on it find steadiness through change. class ImportConfigForm(BaseModel): @@ -112,9 +110,7 @@ async def import_config(request: Request, form_data: ImportConfigForm, user=Depe return await Config.get_all() -############################ -# ExportConfig -############################ +# Export Config @router.get('/export', response_model=dict) @@ -145,9 +141,7 @@ async def set_interface_visibility(request: Request, form_data: InterfaceVisibil return form_data -############################ # Connections Config -############################ class ConnectionsConfigForm(BaseModel): @@ -232,9 +226,7 @@ async def register_oauth_client( ) -############################ # ToolServers Config -############################ class ToolServerConnection(BaseModel): @@ -718,9 +710,7 @@ async def verify_tool_servers_config(request: Request, form_data: ToolServerConn ) -############################ -# CodeInterpreterConfig -############################ +# Code Interpreter Config class CodeInterpreterConfigForm(BaseModel): ENABLE_CODE_EXECUTION: bool CODE_EXECUTION_ENGINE: str @@ -766,9 +756,7 @@ async def set_code_execution_config( return values -############################ -# SetDefaultModels -############################ +# Set Default Models class ModelsConfigForm(BaseModel): DEFAULT_MODELS: str | None DEFAULT_PINNED_MODELS: str | None @@ -878,9 +866,7 @@ async def set_default_suggestions( return {'suggestions': suggestions, 'i18n': suggestions_i18n} -############################ -# SetBanners -############################ +# Set Banners class SetBannersForm(BaseModel): diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index 9dd8413077..3dd1bd5c7d 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -41,27 +41,10 @@ async def get_config_values(key_map: dict[str, str]) -> dict: return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values} -# Leaderboard Elo Rating Computation -# The judgment has already been rendered with grace; -# the scales have been balanced by a hand that never errs. -# -# How it works: -# 1. Each model starts with a rating of 1000 -# 2. When a user picks a winner between two models, ratings are adjusted: -# - Winner gains points, loser loses points -# - The amount depends on expected outcome (upset = bigger change) -# 3. The Elo formula: new_rating = old_rating + K * (actual - expected) -# - K=32 controls how much ratings can change per match -# - expected = probability of winning based on current ratings -# -# Query-based re-ranking (optional): -# When a user searches for a topic (e.g., "coding"), we want to show -# which models perform best FOR THAT TOPIC. We do this by: -# 1. Computing semantic similarity between the query and each feedback's tags -# 2. Using that similarity as a weight in the Elo calculation -# 3. Feedbacks about "coding" contribute more to the final ranking -# 4. Feedbacks about unrelated topics (e.g., "cooking") contribute less -# This gives topic-specific leaderboards without needing separate data. +# Let the scales be fair even when our favorite loses, +# and let a better score never go straight to our heads. + +# Elo starts at 1000 with K=32; query similarity optionally weights each match. import os @@ -277,9 +260,7 @@ async def get_model_history( return ModelHistoryResponse(model_id=model_id, history=history) -############################ -# GetConfig -############################ +# Get Config @router.get('/config') @@ -287,9 +268,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)): return await get_config_values(EVALUATION_CONFIG_KEYS) -############################ -# UpdateConfig -############################ +# Update Config class UpdateConfigForm(BaseModel): diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index f583b38880..8222c53782 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -57,11 +57,9 @@ router = APIRouter() from open_webui.utils.access_control.files import has_access_to_file from open_webui.utils.json_codec import JSONCodec -############################ # Upload File # What was entrusted here was given in good faith. Let it # be returned the same way, whole and undiminished. -############################ def _is_text_file(file_path: str, chunk_size: int = 8192) -> bool: @@ -183,10 +181,7 @@ async def process_uploaded_file( ) ): if content_type.startswith('video/'): - # Videos are stored as-is for downstream multimodal - # processing (Tools, vision models). Attempting text - # extraction causes "Timeout reached while detecting - # encoding" errors. + # Keep videos for multimodal processing; text extraction can time out detecting their encoding. log.info('Video file detected (%s), skipping text extraction', content_type) else: raise Exception(f'File type {content_type} is not supported for processing') @@ -507,9 +502,7 @@ async def upload_file_handler( ) -############################ # List Files -############################ PAGE_SIZE = 50 @@ -535,9 +528,7 @@ async def list_files( return result -############################ # Search Files -############################ async def _search_files(filename: str, content: bool, skip: int, limit: int, user_id: str | None, db: AsyncSession): @@ -583,9 +574,7 @@ async def search_user_files( return await _search_files(filename, content, skip, limit, user.id, db) -############################ # Count Files -############################ @router.get('/count', response_model=int) @@ -605,9 +594,7 @@ async def count_user_files( return await Files.count_files_by_user_id(user_id=user.id, db=db) -############################ # Delete All Files -############################ @router.delete('/all') @@ -635,9 +622,7 @@ async def delete_all_files( ) -############################ # Get File By Id -############################ @router.get('/{id}', response_model=Optional[FileModel]) @@ -665,10 +650,7 @@ async def get_file_process_status( stream: bool = Query(False), user=Depends(get_verified_user), ): - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations manage their own short-lived sessions internally. - # Holding a session here would keep a connection for the entire stream - # (up to two hours) and exhaust the connection pool under concurrent load. + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. file = await Files.get_file_by_id(id) if not file: @@ -718,9 +700,7 @@ async def get_file_process_status( ) -############################ # Get File Data Content By Id -############################ @router.get('/{id}/data/content') @@ -744,9 +724,7 @@ async def get_file_data_content_by_id( ) -############################ # Update File Data Content By Id -############################ class ContentForm(BaseModel): @@ -838,9 +816,7 @@ async def update_file_data_content_by_id( ) -############################ # Get File Content By Id -############################ @router.get('/{id}/content') @@ -1008,9 +984,7 @@ async def get_file_content_by_id( ) -############################ # Rename File By Id -############################ class FileRenameForm(BaseModel): @@ -1056,9 +1030,7 @@ async def rename_file_by_id( ) -############################ # Delete File By Id -############################ @router.delete('/{id}') diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index d1dd1aa13d..5ddd359230 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -84,9 +84,7 @@ async def check_folders_permission(request: Request, user, db=None): ) -############################ # Get Folders -############################ @router.get('/', response_model=list[FolderNameIdResponse]) @@ -142,9 +140,7 @@ async def get_folders( ] -############################ # Create Folder -############################ @router.post('/') @@ -234,9 +230,7 @@ async def create_folder( ) -############################ # Get Shared Folders -############################ @router.get('/shared') @@ -287,9 +281,7 @@ async def get_shared_folders( return results -############################ # Get Folders By Id -############################ class FolderResponse(FolderModel): @@ -327,9 +319,7 @@ async def get_folder_by_id( ) -############################ # Update Folder Name By Id -############################ @router.post('/{id}/update') @@ -416,9 +406,7 @@ async def update_folder_name_by_id( ) -############################ # Update Folder Parent Id By Id -############################ class FolderParentIdForm(BaseModel): @@ -478,9 +466,7 @@ async def update_folder_parent_id_by_id( ) -############################ # Update Folder Is Expanded By Id -############################ class FolderIsExpandedForm(BaseModel): @@ -522,9 +508,7 @@ async def update_folder_is_expanded_by_id( ) -############################ # Update Folder Access By Id -############################ class FolderAccessGrantsForm(BaseModel): @@ -584,9 +568,7 @@ async def update_folder_access_by_id( } -############################ # Get Shared Folder Chats -############################ @router.get('/{id}/shared/chats') @@ -702,9 +684,7 @@ async def mark_folder_chats_read_by_id( } -############################ # Delete Folder By Id -############################ @router.delete('/{id}') diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 86d8a4fff6..3eee55ccab 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -40,11 +40,9 @@ log = logging.getLogger(__name__) router = APIRouter() -############################ -# GetFunctions -# Our daily functions give us, and forgive us -# our deprecated methods, as we refactor those who depend on us. -############################ +# Get Functions +# Let the work begun here be faithful to its promise, +# and let it serve those who call upon it, quietly and well. @router.get('/', response_model=list[FunctionResponse]) @@ -63,9 +61,7 @@ async def get_function_list(user=Depends(get_admin_user), db: AsyncSession = Dep return await Functions.get_function_list(db=db) -############################ -# ExportFunctions -############################ +# Export Functions @router.get('/export', response_model=list[FunctionModel | FunctionWithValvesModel]) @@ -80,9 +76,7 @@ async def get_functions( return await Functions.get_functions(include_valves=include_valves, db=db) -############################ -# LoadFunctionFromLink -############################ +# Load Function From Link class LoadUrlForm(BaseModel): @@ -153,9 +147,7 @@ async def load_function_from_url(request: Request, form_data: LoadUrlForm, user= ) -############################ -# SyncFunctions -############################ +# Sync Functions class SyncFunctionsForm(BaseModel): @@ -199,9 +191,7 @@ async def sync_functions( ) -############################ -# CreateNewFunction -############################ +# Create New Function @router.post('/create', response_model=FunctionResponse | None) @@ -267,9 +257,7 @@ async def create_new_function( ) -############################ -# GetFunctionById -############################ +# Get Function By Id @router.get('/id/{id}', response_model=FunctionModel | None) @@ -285,9 +273,7 @@ async def get_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes ) -############################ -# ToggleFunctionById -############################ +# Toggle Function By Id @router.post('/id/{id}/toggle', response_model=FunctionModel | None) @@ -338,9 +324,7 @@ async def toggle_function_by_id( ) -############################ -# ToggleGlobalById -############################ +# Toggle Global By Id @router.post('/id/{id}/toggle/global', response_model=FunctionModel | None) @@ -375,9 +359,7 @@ async def toggle_global_by_id( ) -############################ -# UpdateFunctionById -############################ +# Update Function By Id @router.post('/id/{id}/update', response_model=FunctionModel | None) @@ -433,9 +415,7 @@ async def _update_function(request, id, form_data, user, db, version_id=None): ) -############################ -# DeleteFunctionById -############################ +# Delete Function By Id @router.delete('/id/{id}/delete', response_model=bool) @@ -462,9 +442,7 @@ async def delete_function_by_id( return result -############################ -# GetFunctionValves -############################ +# Get Function Valves @router.get('/id/{id}/valves', response_model=dict | None) @@ -488,9 +466,7 @@ async def get_function_valves_by_id( ) -############################ -# GetFunctionValvesSpec -############################ +# Get Function Valves Spec @router.get('/id/{id}/valves/spec', response_model=dict | None) @@ -518,9 +494,7 @@ async def get_function_valves_spec_by_id( ) -############################ -# UpdateFunctionValves -############################ +# Update Function Valves @router.post('/id/{id}/valves/update', response_model=dict | None) @@ -570,9 +544,7 @@ async def update_function_valves_by_id( ) -############################ -# FunctionUserValves -############################ +# Function User Valves @router.get('/id/{id}/valves/user', response_model=dict | None) diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py index 5cbb8840bd..f387806381 100755 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -40,9 +40,7 @@ log = logging.getLogger(__name__) router = APIRouter() -############################ -# GetFunctions -############################ +# Get Functions @router.get('/', response_model=list[GroupResponse]) @@ -64,9 +62,7 @@ async def get_groups( return groups -############################ -# CreateNewGroup -############################ +# Create New Group @router.post('/create', response_model=Optional[GroupResponse]) @@ -107,9 +103,7 @@ async def create_new_group( ) -############################ -# GetGroupById -############################ +# Get Group By Id @router.get('/id/{id}', response_model=Optional[GroupResponse]) @@ -142,9 +136,7 @@ async def get_group_info_by_id(id: str, user=Depends(get_verified_user), db: Asy ) -############################ -# ExportGroupById -############################ +# Export Group By Id class GroupExportResponse(GroupResponse): @@ -168,9 +160,7 @@ async def export_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes ) -############################ -# GetUsersInGroupById -############################ +# Get Users In Group By Id @router.post('/id/{id}/users', response_model=list[UserInfoResponse]) @@ -186,9 +176,7 @@ async def get_users_in_group(id: str, user=Depends(get_admin_user), db: AsyncSes ) -############################ -# UpdateGroupById -############################ +# Update Group By Id @router.post('/id/{id}/update', response_model=Optional[GroupResponse]) @@ -231,9 +219,7 @@ async def update_group_by_id( ) -############################ -# AddUserToGroupByUserIdAndGroupId -############################ +# Add User To Group By User Id And Group Id @router.post('/id/{id}/users/add', response_model=Optional[GroupResponse]) @@ -317,9 +303,7 @@ async def remove_users_from_group( ) -############################ -# DeleteGroupById -############################ +# Delete Group By Id @router.delete('/id/{id}/delete', response_model=bool) @@ -363,9 +347,7 @@ async def delete_group_by_id( ) -############################ -# PreviewGroupAccess -############################ +# Preview Group Access @router.get('/id/{id}/preview') diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index bae8b13b73..c083e4e477 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -53,9 +53,7 @@ log = logging.getLogger(__name__) router = APIRouter() -############################ # getKnowledgeBases -############################ PAGE_ITEM_COUNT = 30 @@ -79,9 +77,7 @@ async def delete_file_resource(file: FileModel, db: AsyncSession) -> bool: return result -############################ # Knowledge Base Embedding -############################ # Knowledge that sits unread serves no one. Let what is # stored here find the ones who need it. @@ -311,9 +307,7 @@ async def search_knowledge_files( return await Knowledges.search_knowledge_files(filter=filter, skip=skip, limit=limit, db=db) -############################ -# CreateNewKnowledge -############################ +# Create New Knowledge @router.post('/create', response_model=KnowledgeResponse | None) @@ -322,10 +316,7 @@ async def create_new_knowledge( form_data: KnowledgeForm, user=Depends(get_verified_user), ): - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (has_permission, filter_allowed_access_grants, insert_new_knowledge) manage their own sessions. - # This prevents holding a connection during embed_knowledge_base_metadata() - # which makes external embedding API calls (1-5+ seconds). + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. if user.role != 'admin' and not await has_permission( user.id, 'workspace.knowledge', await Config.get('user.permissions') ): @@ -367,9 +358,7 @@ async def create_new_knowledge( ) -############################ -# ReindexKnowledgeFiles -############################ +# Reindex Knowledge Files @router.post('/reindex', response_model=bool) @@ -462,9 +451,7 @@ async def reindex_knowledge_files( return True -############################ -# ReindexKnowledgeBases -############################ +# Reindex Knowledge Bases @router.post('/metadata/reindex', response_model=dict) @@ -495,9 +482,7 @@ async def reindex_knowledge_base_metadata_embeddings( return {'total': len(knowledge_bases), 'success': success_count} -############################ # External Knowledge Sources -############################ class ExternalKnowledgeSourceForm(BaseModel): @@ -1079,9 +1064,7 @@ async def update_external_knowledge_source( return updated -############################ -# GetKnowledgeById -############################ +# Get Knowledge By Id class KnowledgeFilesResponse(KnowledgeResponse): @@ -1131,9 +1114,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user), db: Asyn ) -############################ -# UpdateKnowledgeById -############################ +# Update Knowledge By Id @router.post('/{id}/update', response_model=KnowledgeFilesResponse | None) @@ -1143,10 +1124,7 @@ async def update_knowledge_by_id( form_data: KnowledgeForm, user=Depends(get_verified_user), ): - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations manage their own short-lived sessions internally. - # This prevents holding a connection during embed_knowledge_base_metadata() - # which makes external embedding API calls (1-5+ seconds). + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. knowledge = await Knowledges.get_knowledge_by_id(id=id) if not knowledge: raise HTTPException( @@ -1203,9 +1181,7 @@ async def update_knowledge_by_id( ) -############################ -# UpdateKnowledgeAccessById -############################ +# Update Knowledge Access By Id class KnowledgeAccessGrantsForm(BaseModel): @@ -1267,9 +1243,7 @@ async def update_knowledge_access_by_id( return response -############################ -# GetPendingKnowledgeFiles -############################ +# Get Pending Knowledge Files @router.get('/{id}/files/pending') @@ -1289,10 +1263,7 @@ async def get_pending_knowledge_files( When ``stream=true``, returns an SSE stream that polls every 3 seconds and emits the current pending file list. Closes when no files remain. """ - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations manage their own short-lived sessions internally. - # Holding a session here would keep a connection for the entire stream - # (up to an hour) and exhaust the connection pool under concurrent load. + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. knowledge = await Knowledges.get_knowledge_by_id(id=id) if not knowledge: raise HTTPException( @@ -1350,9 +1321,7 @@ async def get_pending_knowledge_files( ) -############################ -# GetKnowledgeFilesById -############################ +# Get Knowledge Files By Id @router.get('/{id}/files', response_model=KnowledgeFileListResponse) @@ -1419,9 +1388,7 @@ async def get_knowledge_files_by_id( return await Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db) -############################ -# AddFileToKnowledge -############################ +# Add File To Knowledge async def check_knowledge_upload_limits( @@ -1681,9 +1648,7 @@ async def update_file_from_knowledge_by_id( ) -############################ -# RemoveFileFromKnowledge -############################ +# Remove File From Knowledge @router.post('/{id}/file/remove', response_model=KnowledgeFilesResponse | None) @@ -1770,9 +1735,7 @@ async def remove_file_from_knowledge_by_id( ) -############################ -# DeleteKnowledgeById -############################ +# Delete Knowledge By Id @router.delete('/{id}/delete', response_model=bool) @@ -1860,9 +1823,7 @@ async def delete_knowledge_by_id( return result -############################ -# ResetKnowledgeById -############################ +# Reset Knowledge By Id @router.post('/{id}/reset', response_model=KnowledgeResponse | None) @@ -1922,9 +1883,7 @@ async def reset_knowledge_by_id( return knowledge -############################ -# SyncKnowledgeDiff -############################ +# Sync Knowledge Diff class FileManifestEntry(BaseModel): @@ -1961,7 +1920,7 @@ async def sync_knowledge_diff( """ await _verify_knowledge_write_access(id, user, db) - # ── Index existing state ── + # Index existing state existing_directories = await Knowledges.get_all_directories(id, db=db) # Build directory path lookups @@ -2002,7 +1961,7 @@ async def sync_knowledge_diff( 'checksum': stored_checksum, } - # ── Diff files ── + # Diff files added: list[dict] = [] modified: list[dict] = [] deleted: list[dict] = [] @@ -2046,7 +2005,7 @@ async def sync_knowledge_diff( db=db, ) - # ── Diff directories ── + # Diff directories required_directory_paths: set[str] = set() for entry in form_data.manifest: if entry.path: @@ -2070,9 +2029,7 @@ async def sync_knowledge_diff( ) -############################ -# SyncKnowledgeCleanup -############################ +# Sync Knowledge Cleanup class SyncCleanupForm(BaseModel): @@ -2093,7 +2050,7 @@ async def sync_knowledge_cleanup( """ await _verify_knowledge_write_access(id, user, db) - # ── Remove deleted files ── + # Remove deleted files for file_id in form_data.file_ids: file = await Files.get_file_by_id(file_id, db=db) if not file: @@ -2120,7 +2077,7 @@ async def sync_knowledge_cleanup( ): await delete_file_resource(file, db) - # ── Remove orphaned directories (children before parents) ── + # Remove orphaned directories (children before parents) for dir_id in reversed(form_data.dir_ids): # Only delete directories that belong to this knowledge base. directory = await Knowledges.get_directory_by_id(dir_id, db=db) @@ -2131,9 +2088,7 @@ async def sync_knowledge_cleanup( return {'status': True} -############################ -# AddFilesToKnowledge -############################ +# Add Files To Knowledge @router.post('/{id}/files/batch/add', response_model=KnowledgeFilesResponse | None) @@ -2276,9 +2231,7 @@ async def add_files_to_knowledge_batch( ) -############################ -# ExportKnowledgeById -############################ +# Export Knowledge By Id @router.get('/{id}/export') @@ -2324,9 +2277,7 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Asyn ) -############################ # Directory endpoints -############################ class KnowledgeDirectoryCreateForm(BaseModel): diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index b4d1c346b4..dc25625864 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -45,11 +45,9 @@ async def check_memories_permission(user): ) -############################ -# GetMemories +# Get Memories # Let what is remembered here spare someone the cost # of learning it twice. -############################ @router.get('/', response_model=list[MemoryModel]) @@ -63,9 +61,7 @@ async def get_memories( return await Memories.get_memories_by_user_id(user.id, db=db) -############################ -# AddMemory -############################ +# Add Memory class AddMemoryForm(BaseModel): @@ -316,9 +312,7 @@ async def update_memories( return response -############################ -# QueryMemory -############################ +# Query Memory class QueryMemoryForm(BaseModel): @@ -332,10 +326,7 @@ async def query_memory( form_data: QueryMemoryForm, user=Depends(get_verified_user), ): - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (get_memories_by_user_id) manage their own short-lived sessions. - # This prevents holding a connection during EMBEDDING_FUNCTION() - # which makes external embedding API calls (1-5+ seconds). + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. await check_memories_permission(user) memories = await Memories.get_memories_by_user_id(user.id) @@ -352,12 +343,7 @@ async def query_memory( limit=form_data.k, ) - # Filter results by relevance threshold to avoid returning unrelated - # memories. Vector similarity search always returns the top-K nearest - # neighbours even when they are completely irrelevant; applying the - # same RELEVANCE_THRESHOLD used by RAG ensures only genuinely matching - # memories are surfaced (distances are normalised to 0→1, higher is - # better). + # Apply the RAG relevance threshold; top-K results can include unrelated memories. relevance_threshold = await Config.get('rag.relevance_threshold', 0.0) if results and relevance_threshold > 0.0 and results.distances and results.distances[0]: from open_webui.retrieval.vector.main import SearchResult @@ -442,9 +428,7 @@ async def read_memory_path( } -############################ -# ReindexMemoryVectorDB -############################ +# Reindex Memory Vector DB @router.post('/reindex') async def reindex_memories_from_vector_db( request: Request, @@ -506,9 +490,7 @@ async def reset_memory_from_vector_db( return True -############################ -# DeleteMemoriesByUserId -############################ +# Delete Memories By User Id @router.delete('/delete/user', response_model=bool) @@ -538,9 +520,7 @@ async def delete_memory_by_user_id( return False -############################ -# UpdateMemoryById -############################ +# Update Memory By Id @router.post('/{memory_id}/update', response_model=MemoryModel | None) @@ -550,10 +530,7 @@ async def update_memory_by_id( form_data: MemoryUpdateModel, user=Depends(get_verified_user), ): - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (update_memory_by_id_and_user_id) manage their own - # short-lived sessions. This prevents holding a connection during - # EMBEDDING_FUNCTION() which makes external API calls (1-5+ seconds). + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. await check_memories_permission(user) content = clean_memory_content(form_data.content) if form_data.content is not None else None @@ -602,9 +579,7 @@ async def update_memory_by_id( return memory -############################ -# DeleteMemoryById -############################ +# Delete Memory By Id @router.delete('/{memory_id}', response_model=bool) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index f9e7ed74dc..91558b0c04 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -214,11 +214,7 @@ async def _verify_knowledge_file_access( ) -########################### -# GetModels -# Let each model here be judged by what it does and not -# by what it claims. The house deserves honest servants. -########################### +# Get Models PAGE_ITEM_COUNT = 30 @@ -302,9 +298,7 @@ async def get_models( ) -########################### -# GetBaseModels -########################### +# Get Base Models @router.get('/all', response_model=list[ModelResponse]) @@ -327,9 +321,7 @@ async def get_base_models( return await Models.get_base_models(tag=tag, db=db) -########################### -# GetModelTags -########################### +# Get Model Tags @router.get('/tags', response_model=list[str]) @@ -342,9 +334,7 @@ async def get_model_tags(user=Depends(get_verified_user), db: AsyncSession = Dep return sorted(tags) -############################ -# CreateNewModel -############################ +# Create New Model @router.post('/create', response_model=ModelModel | None) @@ -436,9 +426,7 @@ async def create_new_model( return model_response(model) -############################ -# ExportModels -############################ +# Export Models class ModelExportResponse(ModelModel): @@ -496,9 +484,7 @@ async def export_models( return exported -############################ -# ImportModels -############################ +# Import Models class ModelsImportForm(BaseModel): @@ -737,9 +723,7 @@ async def import_models( raise HTTPException(status_code=500, detail=str(e)) -############################ -# SyncModels -############################ +# Sync Models class SyncModelsForm(BaseModel): @@ -776,9 +760,7 @@ async def sync_models( return models -########################### -# GetModelById -########################### +# Get Model By Id class ModelIdForm(BaseModel): @@ -835,9 +817,7 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes ) -########################### -# GetModelById -########################### +# Get Model By Id async def authorized_model_history(id, user, db): @@ -1084,9 +1064,7 @@ async def get_model_profile_image( ) -############################ -# ToggleModelById -############################ +# Toggle Model By Id @router.post('/model/toggle', response_model=ModelResponse | None) @@ -1135,9 +1113,7 @@ async def toggle_model_by_id( ) -############################ -# UpdateModelById -############################ +# Update Model By Id @router.post('/model/update', response_model=ModelModel | None) @@ -1212,9 +1188,7 @@ async def update_model_by_id( return model_response(model) -############################ -# UpdateModelAccessById -############################ +# Update Model Access By Id class ModelAccessGrantsForm(BaseModel): @@ -1294,9 +1268,7 @@ async def update_model_access_by_id( return model_response(model) -############################ -# DeleteModelById -############################ +# Delete Model By Id @router.post('/model/delete', response_model=bool) diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 04a447e96c..f0438425b5 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -95,9 +95,7 @@ async def leave_note_rooms_for_revoked_users( await leave_room_for_users(room, user_ids) -############################ -# GetNotes -############################ +# Get Notes class NoteItemResponse(BaseModel): @@ -148,9 +146,7 @@ async def get_notes( ] -############################ -# GetPinnedNotes -############################ +# Get Pinned Notes @router.get('/pinned', response_model=list[NoteItemResponse]) @@ -229,9 +225,7 @@ async def search_notes( return result -############################ -# CreateNewNote -############################ +# Create New Note @router.post('/create', response_model=Optional[NoteModel]) @@ -270,9 +264,7 @@ async def create_new_note( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# GetNoteById -############################ +# Get Note By Id class NoteResponse(NoteModel): @@ -521,9 +513,7 @@ async def create_note_chat_by_id( return chat -############################ -# UpdateNoteById -############################ +# Update Note By Id @router.post('/{id}/update', response_model=Optional[NoteModel]) @@ -609,9 +599,7 @@ async def update_note_pages( return await update_note_by_id(request, id, NoteUpdateForm(page_operation=form_data), user, db) -############################ -# UpdateNoteAccessById -############################ +# Update Note Access By Id class NoteAccessGrantsForm(BaseModel): @@ -667,9 +655,7 @@ async def update_note_access_by_id( return note -############################ -# PinNoteById -############################ +# Pin Note By Id @router.post('/{id}/pin', response_model=Optional[NoteModel]) @@ -710,9 +696,7 @@ async def pin_note_by_id( return note -############################ -# DeleteNoteById -############################ +# Delete Note By Id @router.delete('/{id}/delete', response_model=bool) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 2d2b0fe397..40c4fd6b59 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -189,11 +189,7 @@ def get_api_key(idx, url, configs): return configs.get(str(idx), configs.get(base_url, {})).get('key', None) # Legacy support -########################################## -# # API routes -# -########################################## router = APIRouter() @@ -1138,10 +1134,7 @@ async def generate_chat_completion( if not await Config.get('ollama.enable'): raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. - # This prevents holding a connection during the entire LLM call (30-60+ seconds), - # which would exhaust the connection pool under concurrent load. + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. # bypass_filter and bypass_system_prompt are read from request.state to prevent # external clients from setting them via query parameter. Only internal @@ -1254,10 +1247,7 @@ async def generate_openai_completion( user=Depends(get_verified_user), # noqa: B008 ): """Forward a text completion request via the OpenAI-compatible proxy.""" - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. - # This prevents holding a connection during the entire LLM call (30-60+ seconds), - # which would exhaust the connection pool under concurrent load. + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. metadata = form_data.pop('metadata', None) try: @@ -1359,10 +1349,7 @@ async def generate_openai_chat_completion( user=Depends(get_verified_user), # noqa: B008 ): """Forward a chat completion request via the OpenAI-compatible proxy.""" - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. - # This prevents holding a connection during the entire LLM call (30-60+ seconds), - # which would exhaust the connection pool under concurrent load. + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. metadata = form_data.pop('metadata', None) try: diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 55b06eb42a..148f05d2d8 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -60,13 +60,9 @@ from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -########################################## -# # Utility functions # Let the responses returned through this gate be worth # the question that summoned them. -# -########################################## # Headers that become stale after aiohttp auto-decompresses the upstream # response body. Forwarding them verbatim causes desktop / programmatic @@ -150,11 +146,7 @@ def openai_reasoning_model_handler(payload): return payload -########################################## -# # API routes -# -########################################## router = APIRouter() @@ -1412,10 +1404,7 @@ async def generate_chat_completion( if not await Config.get('openai.enable'): raise HTTPException(status_code=503, detail='OpenAI API is disabled') - # NOTE: We intentionally do NOT use Depends(get_async_session) here. - # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. - # This prevents holding a connection during the entire LLM call (30-60+ seconds), - # which would exhaust the connection pool under concurrent load. + # Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections. # bypass_filter and bypass_system_prompt are read from request.state to prevent # external clients from setting them via query parameter. Only internal diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py index 753c9fc77f..1fe70a1dba 100644 --- a/backend/open_webui/routers/pipelines.py +++ b/backend/open_webui/routers/pipelines.py @@ -29,13 +29,9 @@ from starlette.responses import FileResponse log = logging.getLogger(__name__) -################################## -# # Pipeline Middleware # Every hand this passes through can corrupt it or # improve it. Let each stage leave it better than it found. -# -################################## def get_sorted_filters(model_id, models): @@ -192,11 +188,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models): return payload -################################## -# # Pipelines Endpoints -# -################################## router = APIRouter() diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 28b1e7b685..fb8fdc0f9a 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -44,11 +44,9 @@ router = APIRouter() PAGE_ITEM_COUNT = 30 -############################ -# GetPrompts +# Get Prompts # The hardest part is knowing what to ask. Let the right # question already be here when it is needed. -############################ @router.get('/', response_model=list[PromptModel]) @@ -135,9 +133,7 @@ async def get_prompt_list( ) -############################ -# CreateNewPrompt -############################ +# Create New Prompt @router.post('/create', response_model=PromptModel | None) @@ -197,9 +193,7 @@ async def create_new_prompt( ) -############################ -# GetPromptById -############################ +# Get Prompt By Id @router.get('/id/{prompt_id}', response_model=PromptAccessResponse | None) @@ -241,9 +235,7 @@ async def get_prompt_by_id( ) -############################ -# UpdatePromptById -############################ +# Update Prompt By Id @router.post('/id/{prompt_id}/update', response_model=PromptModel | None) @@ -307,9 +299,7 @@ async def update_prompt_by_id( ) -############################ -# UpdatePromptMetadata -############################ +# Update Prompt Metadata @router.post('/id/{prompt_id}/update/meta', response_model=PromptModel | None) @@ -421,9 +411,7 @@ async def set_prompt_version( ) -############################ -# UpdatePromptAccessById -############################ +# Update Prompt Access By Id class PromptAccessGrantsForm(BaseModel): @@ -482,9 +470,7 @@ async def update_prompt_access_by_id( return updated_prompt -############################ -# TogglePromptActiveById -############################ +# Toggle Prompt Active By Id @router.post('/id/{prompt_id}/toggle', response_model=PromptModel | None) @@ -535,9 +521,7 @@ async def toggle_prompt_active( ) -############################ -# DeletePromptById -############################ +# Delete Prompt By Id @router.delete('/id/{prompt_id}/delete', response_model=bool) @@ -583,9 +567,7 @@ async def delete_prompt_by_id( return result -############################ # Prompt History Endpoints -############################ @router.get('/id/{prompt_id}/history', response_model=list[PromptHistoryResponse]) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index fe1db69ace..072a3d2df1 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -135,13 +135,9 @@ log = logging.getLogger(__name__) TIKTOKEN_DISALLOWED_SPECIAL = () -########################################## -# # Utility functions -# Give us this day our relevant chunks, and lead us -# not into hallucination, but deliver us from noise. -# -########################################## +# Let us find what is true among the many voices, +# and let us not mistake the loudest answer for the wisest. def get_ef( @@ -240,11 +236,7 @@ def get_rf( return rf -########################################## -# # API routes -# -########################################## RETRIEVAL_CONFIG_KEYS = { @@ -1243,11 +1235,7 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend } -#################################### -# # Document process and retrieval -# -#################################### def can_merge_chunks(a: Document, b: Document) -> bool: @@ -2964,11 +2952,7 @@ async def query_collection_handler( ) -#################################### -# # Vector DB operations -# -#################################### class DeleteForm(BaseModel): @@ -3006,13 +2990,7 @@ async def delete_entries_from_collection( detail=ERROR_MESSAGES.DEFAULT('File has no hash; cannot delete vector entries by hash.'), ) - # Pre-existing bug: this used `metadata=` which is not a - # parameter on `VectorDBBase.delete` nor on any backend - # implementation, so the call always raised TypeError that - # was silently swallowed by the surrounding `except - # Exception` and the endpoint reported `{'status': False}` - # for every request. Use `filter` to actually do what the - # endpoint name promises. + # Vector deletion accepts filter, not metadata; passing the wrong keyword raises TypeError. await ASYNC_VECTOR_DB_CLIENT.delete( collection_name=form_data.collection_name, filter={'hash': hash}, diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index d8ffb87f1d..aaefd49df6 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -48,9 +48,7 @@ PAGE_ITEM_COUNT = 30 router = APIRouter() -############################ -# GetSkills -############################ +# Get Skills @router.get('/', response_model=list[SkillUserResponse]) @@ -72,9 +70,7 @@ async def get_skills( return skills -############################ -# GetSkillList -############################ +# Get Skill List @router.get('/list', response_model=SkillAccessListResponse) @@ -134,9 +130,7 @@ async def get_skill_list( ) -############################ -# ExportSkills -############################ +# Export Skills async def authorized_skill(id, user, permission='read', db=None): @@ -211,9 +205,7 @@ async def export_skills( raise HTTPException(400, str(error)) -############################ -# CreateNewSkill -############################ +# Create New Skill @router.post('/create', response_model=Optional[SkillResponse]) @@ -290,9 +282,7 @@ async def create_new_skill( ) -############################ -# GetSkillById -############################ +# Get Skill By Id @router.get('/id/{id}', response_model=Optional[SkillDetailResponse]) @@ -337,9 +327,7 @@ async def get_skill_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes ) -############################ -# UpdateSkillById -############################ +# Update Skill By Id @router.post('/id/{id}/update', response_model=Optional[SkillModel]) @@ -403,9 +391,7 @@ async def update_skill_by_id( ) -############################ -# UpdateSkillAccessById -############################ +# Update Skill Access By Id class SkillAccessGrantsForm(BaseModel): @@ -464,9 +450,7 @@ async def update_skill_access_by_id( return skill -############################ -# ToggleSkillById -############################ +# Toggle Skill By Id @router.post('/id/{id}/toggle', response_model=Optional[SkillModel]) @@ -517,9 +501,7 @@ async def toggle_skill_by_id( ) -############################ -# DeleteSkillById -############################ +# Delete Skill By Id @router.delete('/id/{id}/delete', response_model=bool) diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 921809a12f..75943a8764 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -99,11 +99,7 @@ async def get_task_model_generation_config(default_model_id: str, models) -> tup ) -################################## -# # Task Endpoints -# -################################## @router.get('/config') diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index e4af6e09a8..39ca32b02c 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -480,9 +480,7 @@ async def proxy_terminal( return JSONResponse({'error': f'Terminal proxy error: {error}'}, status_code=502) -# --------------------------------------------------------------------------- # WebSocket proxy for interactive terminal sessions -# --------------------------------------------------------------------------- async def _resolve_authenticated_connection(ws: WebSocket, server_id: str): diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 6e77e27168..0c46cdda18 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -70,11 +70,9 @@ async def get_tool_module(request, tool_id, load_from_db=True): return tool_module -############################ -# GetTools +# Get Tools # The danger is not in having tools, but in reaching # for the wrong one. Let the choice here be deliberate. -############################ @router.get('/', response_model=list[ToolUserResponse]) @@ -242,9 +240,7 @@ async def get_tool_specs(request: Request, id: str, user=Depends(get_verified_us raise HTTPException(status_code=502, detail='Unable to load tools') -############################ -# GetToolList -############################ +# Get Tool List class MCPAppRPCForm(BaseModel): @@ -399,9 +395,7 @@ async def toggle_tool_by_id( return tool -############################ -# LoadFunctionFromLink -############################ +# Load Function From Link class LoadUrlForm(BaseModel): @@ -472,9 +466,7 @@ async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depe ) -############################ -# ExportTools -############################ +# Export Tools @router.get('/export', response_model=list[ToolModel]) @@ -502,9 +494,7 @@ async def export_tools( ) -############################ -# CreateNewTools -############################ +# Create New Tools @router.post('/create', response_model=ToolResponse | None) @@ -591,9 +581,7 @@ async def create_new_tools( ) -############################ -# GetToolsById -############################ +# Get Tools By Id @router.get('/id/{id}', response_model=ToolAccessResponse | None) @@ -640,9 +628,7 @@ async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes ) -############################ -# UpdateToolsById -############################ +# Update Tools By Id @router.post('/id/{id}/update', response_model=ToolModel | None) @@ -736,9 +722,7 @@ async def _update_tool(request, id, form_data, user, db, version_id=None): ) -############################ -# UpdateToolAccessById -############################ +# Update Tool Access By Id class ToolAccessGrantsForm(BaseModel): @@ -797,9 +781,7 @@ async def update_tool_access_by_id( return tools -############################ -# DeleteToolsById -############################ +# Delete Tools By Id @router.delete('/id/{id}/delete', response_model=bool) @@ -849,9 +831,7 @@ async def delete_tools_by_id( return result -############################ -# GetToolValves -############################ +# Get Tool Valves @router.get('/id/{id}/valves', response_model=dict | None) @@ -891,9 +871,7 @@ async def get_tools_valves_by_id( ) -############################ -# GetToolValvesSpec -############################ +# Get Tool Valves Spec @router.get('/id/{id}/valves/spec', response_model=dict | None) @@ -937,9 +915,7 @@ async def get_tools_valves_spec_by_id( return None -############################ -# UpdateToolValves -############################ +# Update Tool Valves @router.post('/id/{id}/valves/update', response_model=dict | None) @@ -1002,9 +978,7 @@ async def update_tools_valves_by_id( ) -############################ -# ToolUserValves -############################ +# Tool User Valves @router.get('/id/{id}/valves/user', response_model=dict | None) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 4c3c5bf7e6..6856daa50a 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -72,11 +72,7 @@ def merge_user_ui_settings(defaults: dict, settings: dict) -> dict: return merged -############################ -# GetUsers -# A house is only as strong as its care for the least of -# its members. Let none here be counted without being served. -############################ +# Get Users PAGE_ITEM_COUNT = 30 @@ -164,9 +160,7 @@ async def search_users( ) -############################ # User Groups -############################ @router.get('/groups') @@ -176,9 +170,7 @@ async def get_user_groups( return await user_groups_response(user.id, include_inherited, db) -############################ # User Permissions -############################ @router.get('/permissions') @@ -192,9 +184,7 @@ async def get_user_permissisions( return user_permissions -############################ # User Default Permissions -############################ class WorkspacePermissions(BaseModel): models: bool = False knowledge: bool = False @@ -458,9 +448,7 @@ async def get_default_user_permissions_defaults(user=Depends(get_admin_user)): } -############################ -# GetUserSettingsBySessionUser -############################ +# Get User Settings By Session User @router.get('/user/settings', response_model=UserSettings | None) @@ -483,9 +471,7 @@ async def get_user_settings_by_session_user( return UserSettings.model_validate(user_settings) -############################ -# UpdateUserSettingsBySessionUser -############################ +# Update User Settings By Session User @router.post('/user/settings/update', response_model=UserSettings) @@ -561,9 +547,7 @@ async def update_user_settings_by_session_user( ) -############################ -# GetUserStatusBySessionUser -############################ +# Get User Status By Session User @router.get('/user/status') @@ -580,9 +564,7 @@ async def get_user_status_by_session_user( return user -############################ -# UpdateUserStatusBySessionUser -############################ +# Update User Status By Session User @router.post('/user/status/update') @@ -613,9 +595,7 @@ async def update_user_status_by_session_user( ) -############################ -# GetUserInfoBySessionUser -############################ +# Get User Info By Session User @router.get('/user/info', response_model=dict | None) @@ -632,9 +612,7 @@ class UserVariablesResponse(BaseModel): variables: dict[str, str] = Field(default_factory=dict) -############################ -# GetUserVariablesBySessionUser -############################ +# Get User Variables By Session User @router.get('/user/variables', response_model=UserVariablesResponse) @@ -642,9 +620,7 @@ async def get_user_variables_by_session_user(user=Depends(get_verified_user)): return UserVariablesResponse(variables=normalize_user_variables(user.variables)) -############################ -# UpdateUserVariablesBySessionUser -############################ +# Update User Variables By Session User @router.post('/user/variables/update', response_model=UserVariablesResponse) @@ -670,9 +646,7 @@ async def update_user_variables_by_session_user( return UserVariablesResponse(variables=variables) -############################ -# UpdateUserInfoBySessionUser -############################ +# Update User Info By Session User @router.post('/user/info/update', response_model=dict | None) @@ -697,9 +671,7 @@ async def update_user_info_by_session_user( # PATCH-style merge return updated.info -############################ -# GetUserUsageBySessionUser -############################ +# Get User Usage By Session User @router.get('/usage', response_model=UserUsageResponse) @@ -776,9 +748,7 @@ async def get_user_usage_by_session_user( ) -############################ -# GetUserById -############################ +# Get User By Id class UserActiveResponse(UserStatus): @@ -845,9 +815,7 @@ async def get_user_oauth_sessions_by_id( ) -############################ -# GetUserProfileImageById -############################ +# Get User Profile Image By Id @router.get('/{user_id}/profile/image') @@ -892,9 +860,7 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u ) -############################ -# GetUserActiveStatusById -############################ +# Get User Active Status By Id @router.get('/{user_id}/active', response_model=dict) @@ -906,9 +872,7 @@ async def get_user_active_status_by_id( } -############################ -# UpdateUserById -############################ +# Update User By Id @router.post('/{user_id}/sessions/revoke', response_model=bool) @@ -1114,9 +1078,7 @@ async def update_user_by_id( ) -############################ -# DeleteUserById -############################ +# Delete User By Id @router.delete('/{user_id}', response_model=bool) @@ -1164,9 +1126,7 @@ async def delete_user_by_id( ) -############################ -# GetUserGroupsById -############################ +# Get User Groups By Id @router.get('/{user_id}/groups') @@ -1179,9 +1139,7 @@ async def get_user_groups_by_id( return await user_groups_response(user_id, include_inherited, db) -############################ -# GetUserPreview -############################ +# Get User Preview @router.get('/{user_id}/preview') diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 3ce70bece0..06c23291fb 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -76,7 +76,7 @@ log = logging.getLogger(__name__) # cause, and let every message find the room it was meant for. REDIS = None -# Configure CORS for Socket.IO +# Socket.IO expects a wildcard string rather than a one-item wildcard list. SOCKETIO_CORS_ORIGINS = '*' if CORS_ALLOW_ORIGIN == ['*'] else CORS_ALLOW_ORIGIN # Large notes outgrow the 1 MB default; match uvicorn's 16 MiB websocket limit diff --git a/backend/open_webui/tasks.py b/backend/open_webui/tasks.py index 94b247badb..53781438ea 100644 --- a/backend/open_webui/tasks.py +++ b/backend/open_webui/tasks.py @@ -84,9 +84,7 @@ async def redis_task_heartbeat(app): log.exception('Redis task heartbeat failed') -### ------------------------------ -### REDIS-ENABLED HANDLERS -### ------------------------------ +# Redis-enabled handlers async def redis_save_task(redis: Redis, task_id: str, item_id: str | None): @@ -166,11 +164,11 @@ async def create_task(redis, coroutine, id=None, task_id=None): task_id = task_id or str(uuid4()) # Generate a unique ID for the task task = asyncio.create_task(coroutine) # Create the task - # Add a done callback for cleanup + # Give the task a checkout routine before handing it the keys. task.add_done_callback(lambda t: asyncio.create_task(cleanup_task(redis, task_id, id))) tasks[task_id] = task - # If an ID is provided, associate the task with that ID + # File this task under its item ID, if it came with a return address. if id: if item_tasks.get(id): item_tasks[id].append(task_id) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index e39a81abf2..293ae5993d 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -124,9 +124,7 @@ async def _has_read_access_to_file( ) -# ============================================================================= -# TIME UTILITIES -# ============================================================================= +# Time utilities async def notify( @@ -226,7 +224,7 @@ async def calculate_timestamp( total_days = days_ago + (weeks_ago * 7) adjusted = now - datetime.timedelta(days=total_days) - # Handle months and years separately (variable length) + # Months and years need calendar arithmetic; February refuses to be 30 days. if months_ago > 0 or years_ago > 0: adjusted = adjusted - relativedelta(months=months_ago, years=years_ago) @@ -287,9 +285,7 @@ async def calculate_timestamp( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# WEB SEARCH TOOLS -# ============================================================================= +# Web search tools async def search_web( @@ -363,9 +359,7 @@ async def fetch_url( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# IMAGE GENERATION TOOLS -# ============================================================================= +# Image generation tools async def generate_image( @@ -535,9 +529,7 @@ async def edit_image( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# USER INPUT TOOLS -# ============================================================================= +# User input tools async def ask_user( @@ -652,9 +644,7 @@ async def ask_user( return JSONCodec.dumps({'status': 'error', 'error': str(e)}, ensure_ascii=False) -# ============================================================================= -# CODE INTERPRETER TOOLS -# ============================================================================= +# Code interpreter tools async def execute_code( @@ -819,9 +809,7 @@ async def execute_code( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# MEMORY TOOLS -# ============================================================================= +# Memory tools async def list_memory_paths( @@ -1146,9 +1134,7 @@ async def list_memories( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# NOTES TOOLS -# ============================================================================= +# Notes tools async def search_notes( @@ -1634,9 +1620,7 @@ async def replace_note_content( return JSONCodec.dumps({'error': str(e), 'code': 'unexpected_error'}) -# ============================================================================= -# CHATS TOOLS -# ============================================================================= +# Chats tools async def search_chats( @@ -1750,7 +1734,7 @@ async def view_chat( history = chat.chat.get('history', {}) msg_dict = history.get('messages', {}) - # Build message chain from currentId + # Follow parent links from currentId; conversations have family trees too. current_id = history.get('currentId') visited = set() @@ -1784,9 +1768,7 @@ async def view_chat( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# SUB-AGENT TOOL -# ============================================================================= +# Sub-agent tool async def delegate_task( @@ -1867,9 +1849,7 @@ async def timer( ) -# ============================================================================= -# CHANNELS TOOLS -# ============================================================================= +# Channels tools async def search_channels( @@ -2109,7 +2089,7 @@ async def view_channel_thread( # Build the response messages = [] - # Add parent message first + # Parent message first; the replies need something to reply to. messages.append( { 'id': parent_message.id, @@ -2121,7 +2101,7 @@ async def view_channel_thread( } ) - # Add thread replies (reverse to get chronological order) + # Reverse the replies into chronological order; spoilers go at the end. for reply in reversed(thread_replies): messages.append( { @@ -2150,9 +2130,7 @@ async def view_channel_thread( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# KNOWLEDGE BASE TOOLS -# ============================================================================= +# Knowledge base tools async def list_knowledge_bases( @@ -3664,9 +3642,7 @@ async def query_knowledge_bases( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# SKILLS TOOLS -# ============================================================================= +# Skills tools async def view_skill( @@ -4040,9 +4016,7 @@ async def update_skill_files( return JSONCodec.dumps({'error': detail}) -# ============================================================================= -# TOOL SEARCH -# ============================================================================= +# Tool search async def search_tools( @@ -4072,9 +4046,7 @@ async def search_tools( return JSONCodec.dumps({'tools': [candidates[name] for name in matches]}) -# ============================================================================= -# TASK MANAGEMENT TOOLS -# ============================================================================= +# Task management tools from typing import Literal @@ -4217,9 +4189,7 @@ async def update_task( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# AUTOMATION TOOLS -# ============================================================================= +# Automation tools async def _validate_owned_automation_folder(user_id: str, folder_id: Optional[str]) -> Optional[str]: @@ -4612,9 +4582,7 @@ async def delete_automation( return JSONCodec.dumps({'error': str(e)}) -# ============================================================================= -# CALENDAR TOOLS -# ============================================================================= +# Calendar tools MAX_CALENDAR_RANGE_END_NS = 2**63 - 1 diff --git a/backend/open_webui/tools/knowledge_fs.py b/backend/open_webui/tools/knowledge_fs.py index 0ab071f24a..5bc73396d9 100644 --- a/backend/open_webui/tools/knowledge_fs.py +++ b/backend/open_webui/tools/knowledge_fs.py @@ -62,9 +62,7 @@ def match_budget(): _active_budget.reset(token) -# ============================================================================= -# SHARED REGEX UTILITIES — also used by builtin.py grep_knowledge_files -# ============================================================================= +# Shared regex utilities — also used by builtin.py grep_knowledge_files def is_regex_pattern(pattern: str) -> bool: @@ -173,9 +171,7 @@ def build_matcher(pattern: str, case_insensitive: bool = False, use_regex: bool return (lambda line: sp in (line.lower() if case_insensitive else line)), None -# ============================================================================= -# COMMAND PARSING -# ============================================================================= +# Command parsing def _parse_pipeline(command: str) -> list[list[str]]: @@ -248,9 +244,7 @@ def _extract_numeric_flag(tokens: list[str]) -> tuple[Optional[int], list[str]]: return num, remaining -# ============================================================================= -# DIRECTORY TREE & PATH RESOLUTION -# ============================================================================= +# Directory tree & path resolution async def _build_directory_tree(knowledge_id: str) -> dict: @@ -344,9 +338,7 @@ def _get_files_under_dir(tree: dict, dir_id: str) -> list[dict]: return [f for f in tree['files'] if f['directory_id'] in target_ids] -# ============================================================================= -# FILE RESOLUTION & ACCESS CONTROL -# ============================================================================= +# File resolution & access control async def _get_accessible_kb_ids( @@ -573,9 +565,7 @@ async def _get_file_content(file_id: str) -> str | None: return None -# ============================================================================= -# COMMAND HANDLERS -# ============================================================================= +# Command handlers async def _kb_ls(args: list[str], flags: set[str], user: dict, model_knowledge: list[dict] | None) -> str: @@ -1063,9 +1053,7 @@ async def _kb_sed( return result -# ============================================================================= -# PIPE EXECUTOR -# ============================================================================= +# Pipe executor async def _kb_tree(args: list[str], flags: set[str], user: dict, model_knowledge: list[dict] | None) -> str: @@ -1177,9 +1165,7 @@ async def _execute_pipeline( return piped_input or '' -# ============================================================================= -# ENTRY POINT -# ============================================================================= +# Entry point async def kb_exec( diff --git a/backend/open_webui/utils/anthropic.py b/backend/open_webui/utils/anthropic.py index bd2cea2e61..5c531a466f 100644 --- a/backend/open_webui/utils/anthropic.py +++ b/backend/open_webui/utils/anthropic.py @@ -101,11 +101,7 @@ async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> di return {'object': 'list', 'data': all_models} -############################## -# # Anthropic Messages API Conversion Utilities -# -############################## def _copy_cache_control(source: dict, target: dict) -> dict: @@ -752,7 +748,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str } yield f'event: content_block_delta\ndata: {JSONCodec.dumps(block_delta)}\n\n'.encode() - # --- Handle text content --- + # Handle text content # Anthropic expects text blocks before tool blocks, so skip # text deltas once any tool call has started. content = delta.get('content') @@ -782,7 +778,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str } yield f'event: content_block_delta\ndata: {JSONCodec.dumps(block_delta)}\n\n'.encode() - # --- Handle tool calls --- + # Handle tool calls # Some providers put tool_calls on the final message object # instead of the delta; fall back to that when needed. tool_calls = delta.get('tool_calls') or [] @@ -903,7 +899,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str except (JSONCodec.JSONDecodeError, ValueError): pass - # --- Handle finish reason --- + # Handle finish reason if finish_reason is not None: stop_reason_map = { 'stop': 'end_turn', diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 44d0c8c460..1363601508 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -48,9 +48,7 @@ SESSION_SECRET = WEBUI_SECRET_KEY ALGORITHM = 'HS256' PASSWORD_BCRYPT_MAX_BYTES = 72 -############## # Auth Utils -############## def verify_signature(payload: str, signature: str) -> bool: @@ -222,9 +220,8 @@ async def verify_password(plain_password: str, hashed_password: str) -> bool: return False -# Let the one who signed this token be remembered at every gate, -# and may the claims therein honor the creator long after -# the session has closed. +# Let this token open only the doors it should, +# and have the good manners to expire before overstaying its welcome. def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: payload = data.copy() @@ -391,10 +388,7 @@ async def get_current_user( response: Response, background_tasks: BackgroundTasks, auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), - # NOTE: We intentionally do NOT use Depends(get_session) here. - # Sessions are managed internally with short-lived context managers. - # This ensures connections are released immediately after auth queries, - # not held for the entire request duration (e.g., during 30+ second LLM calls). + # Use short-lived auth sessions so streaming requests do not retain DB connections. ): token = None diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index 0d54e7aa1b..3250b38614 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -56,9 +56,7 @@ TIMER_POLL_INTERVAL = int(os.getenv('TIMER_POLL_INTERVAL', '1')) CALENDAR_ALERT_LOOKAHEAD_MINUTES = int(os.getenv('CALENDAR_ALERT_LOOKAHEAD_MINUTES', '10')) -############################ # Worker Loop -############################ # Keep the old name as an alias so any stale imports still work. @@ -87,7 +85,7 @@ async def scheduler_worker_loop(app) -> None: while True: try: now = time.monotonic() - # ── Timers ── + # Timers try: from open_webui.utils.timers import claim_due_timers, execute_due_timer @@ -102,7 +100,7 @@ async def scheduler_worker_loop(app) -> None: # Jitter to spread automation/calendar load across instances; timers keep a tight poll. next_scheduler_poll = now + SCHEDULER_POLL_INTERVAL + random.uniform(0, 2) - # ── Automations ── + # Automations if await Config.get('automations.enable'): try: async with get_async_db() as db: @@ -114,7 +112,7 @@ async def scheduler_worker_loop(app) -> None: except Exception: log.exception('Scheduler: automation error') - # ── Calendar Alerts ── + # Calendar alerts if await Config.get('calendar.enable'): try: await _check_calendar_alerts(app) @@ -127,9 +125,7 @@ async def scheduler_worker_loop(app) -> None: await asyncio.sleep(max(1, TIMER_POLL_INTERVAL)) -########################## # Execute -#################### def _build_request( @@ -540,9 +536,7 @@ async def execute_automation(app, automation: AutomationModel) -> None: raise -#################### # Internals -#################### async def _check_calendar_alerts(app) -> None: diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 3bc5346b1d..bc7fac1f26 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -207,8 +207,8 @@ async def process_filter_function( return form_data, valves_by_id, skip_files -# Grant these filters the discernment to pass what serves -# and refuse what harms, for every soul in the house. +# Let these filters pass what helps and catch what harms, +# and spare us the bug report that explains the difference. async def process_filter_functions( request, filter_context, diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index a20d53e1bb..0ee6ae5a1b 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -274,10 +274,8 @@ async def publish_chat_failed_event(request: Request, user: UserModel, metadata: ) -# We believe in one maker of all models, seen and unseen, -# and in the reasoning which proceeds from the architect. -# We look for the resurrection of dead processes and the -# inference of the world to come. +# Let every wandering thought find its closing tag, +# and let the answer arrive before we forget the question. DEFAULT_REASONING_TAGS = [ ('', ''), ('', ''), @@ -2435,12 +2433,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): form_data['messages'], ) else: - # Native FC: tool docstring can't be dynamic, so inject - # filesystem context into the system message for pyodide - # engine. Appending to the system prompt (instead of the - # user message) keeps it in the stable cached prefix so - # providers with prefix caching don't re-bill the full - # conversation on every turn. + # Put dynamic Pyodide filesystem context in the system prompt to preserve prefix caching. if engine != 'jupyter': form_data['messages'] = add_or_update_system_message( CODE_INTERPRETER_PYODIDE_PROMPT, @@ -2631,10 +2624,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): prompt = get_last_user_message(form_data['messages']) - # Guard against empty user message after skill mention stripping. - # When a user selects a skill ($skill-name) without typing additional text, - # the stripped result is an empty string which causes 400 errors on providers - # that reject empty content blocks (e.g. AWS Bedrock ConverseStream). + # Supply fallback text for skill-only messages; some providers reject empty content. if not prompt or not prompt.strip(): fallback = ', '.join([s.name for s in available_skills] + [s['name'] for s in terminal_skills]) # Attachment-only messages keep their empty text, same as on models without skills. @@ -4902,11 +4892,7 @@ async def streaming_chat_response_handler(response, ctx): } ) - # Merge any metadata (usage, etc.) - # Strip 'done' — response.completed emits - # it but we may still need to execute tool - # calls. The outer middleware manages the - # actual completion signal. + # Merge metadata without 'done'; the outer middleware signals completion after tool calls. if response_metadata: if ENABLE_RESPONSES_API_STATEFUL: response_id = response_metadata.pop('response_id', None) @@ -5352,13 +5338,7 @@ async def streaming_chat_response_handler(response, ctx): # closure-cell str += recopies per chunk; append + join once at read is O(n) content_parts.append(value) - # Check if we're inside a tag-based block - # (reasoning, code_interpreter, or solution). - # If so, append to the existing in-progress - # item instead of creating a new message — - # otherwise tag_output_handler re-detects the - # start tag on every chunk and fragments the - # output. + # Continue the open tag block so its start tag is not re-detected on each chunk. last_item = output[-1] if output else None last_item_type = last_item.get('type', '') if last_item else '' inside_tag_block = ( @@ -6098,12 +6078,7 @@ async def streaming_chat_response_handler(response, ctx): ) if isinstance(res, StreamingResponse): - # Save accumulated output and start fresh. - # Responses API output_index values are relative - # to the current response — a clean output list - # keeps indices aligned. The display prefix - # ensures the UI shows tool history during - # streaming. + # Reset response-relative output indices; keep prior output as the UI history prefix. prior_output = list(full_output()) # Trim the trailing empty placeholder message # so it doesn't persist as a ghost item once @@ -6392,10 +6367,7 @@ async def streaming_chat_response_handler(response, ctx): except asyncio.CancelledError: log.warning('Task was cancelled!') - # Close the response body iterator to trigger cleanup - # in stream_wrapper's finally block and release the - # upstream connection. Without this, the async - # generator is orphaned and may spin in anyio internals. + # Close the iterator so stream_wrapper's finally block releases the upstream connection. if hasattr(response, 'body_iterator') and hasattr(response.body_iterator, 'aclose'): try: await asyncio.shield(response.body_iterator.aclose()) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 65739bee56..bd65286f0f 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -192,7 +192,7 @@ def get_message_list(messages_map, message_id): :return: List of ordered messages starting from the root to the given message """ - # Handle case where messages is None + # No history, no archaeology. An empty map gets an empty list. if not messages_map: return [] # Return empty list instead of None to prevent iteration errors @@ -202,7 +202,7 @@ def get_message_list(messages_map, message_id): if not current_message: return [] # Return empty list instead of None to prevent iteration errors - # Reconstruct the chain by following the parentId links + # Follow parentId back through the conversation family tree. message_list = [] visited_message_ids = set() @@ -840,25 +840,21 @@ def openai_chat_completion_message_template( def get_gravatar_url(email): - # Trim leading and trailing whitespace from - # an email address and force all characters - # to lower case + # Trim and lowercase the email; the avatar should survive a stuck Shift key. address = str(email).strip().lower() - # Create a SHA256 hash of the final string + # Hash the normalized email into its Gravatar calling card. hash_object = hashlib.sha256(address.encode()) hash_hex = hash_object.hexdigest() - # Grab the actual image URL + # Send the hash to Gravatar; the email stays out of the URL. return f'https://www.gravatar.com/avatar/{hash_hex}?d=mp' -# Give us each day the data we require, and forgive us our -# technical debts as we forgive those who commit upstream. -# Lead the bits not into corruption but deliver them from -# entropy, for the checksum and the glory are forever. +# Let what has been entrusted to us be kept whole, +# and let nothing be lost in the passing from hand to hand. def calculate_sha256(file_path, chunk_size): - # Compute SHA-256 hash of a file efficiently in chunks + # Read in chunks so large files do not need to fit in memory. sha256 = hashlib.sha256() with open(file_path, 'rb') as f: while chunk := f.read(chunk_size): @@ -869,9 +865,9 @@ def calculate_sha256(file_path, chunk_size): def calculate_sha256_string(string): # Create a new SHA-256 hash object sha256_hash = hashlib.sha256() - # Update the hash object with the bytes of the input string + # Feed the hash UTF-8 bytes; it does not take strings on faith. sha256_hash.update(string.encode('utf-8')) - # Get the hexadecimal representation of the hash + # Return the hex fingerprint, now suitable for polite string comparison. hashed_string = sha256_hash.hexdigest() return hashed_string @@ -884,13 +880,13 @@ def validate_email_format(email: str) -> bool: def sanitize_filename(file_name): - # Convert to lowercase + # Lowercase the filename; shouting does not help us find it. lower_case_file_name = file_name.lower() - # Remove special characters using regular expression + # Strip punctuation; word characters and whitespace make the guest list. sanitized_file_name = re.sub(r'[^\w\s]', '', lower_case_file_name) - # Replace spaces with dashes + # Give each whitespace run one dash, regardless of its enthusiasm. final_file_name = re.sub(r'\s+', '-', sanitized_file_name) return final_file_name @@ -1009,7 +1005,7 @@ def extract_folders_after_data_docs(path): except ValueError: return [] - # Exclude the filename and accumulate folder names + # Collect the folder ancestry; the filename is not invited. tags = [] folders = parts[index_docs:-1] @@ -1245,17 +1241,15 @@ def extract_urls(text: str) -> list[str]: return url_pattern.findall(text) -# We believe in one architect of all that is seen and served. -# Should this stream falter, it shall be raised again on the -# third retry. We look for the uptime of the world to come. +# Let what falters have another chance, +# and let the third try be kinder than the first two. async def cleanup_response( response: aiohttp.ClientResponse | None, session: aiohttp.ClientSession | None, ): if response: if not response.closed: - # aiohttp 3.9+ made ClientResponse.close() synchronous (returns None). - # Older versions returned a coroutine. Handle both gracefully. + # Close directly, and await completion if the implementation returns an awaitable. result = response.close() if result is not None: await result diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index d2cd3c3840..40e44f303b 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -353,13 +353,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) functions_by_id = {f.id: f for f in await Functions.get_functions_by_ids(list(all_function_ids))} - # Pre-warm the function module cache once per unique function ID. - # This ensures each function's DB freshness check runs exactly once, - # not once per (model × function) pair. - # Only attempt to load functions that actually exist in the local DB; - # imported/custom model configs may reference tools or filters the user - # hasn't installed, and trying to load those would cause persistent - # "Failed to load function module" log spam on every model refresh. + # Warm each installed function once, avoiding per-model freshness checks and errors for missing functions. for function_id, function in functions_by_id.items(): try: await get_function_module_from_cache(request, function_id, function=function) diff --git a/backend/open_webui/utils/payload.py b/backend/open_webui/utils/payload.py index 50db01021f..b1506e6e45 100644 --- a/backend/open_webui/utils/payload.py +++ b/backend/open_webui/utils/payload.py @@ -65,7 +65,7 @@ async def resolve_system_prompt( # What goes out cannot be taken back. Let it be shaped # well before it leaves this place. -# inplace function: form_data is modified +# Mutates form_data in place. async def apply_system_prompt_to_body( system: Optional[str], form_data: dict, @@ -242,7 +242,7 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict: for key, value in name_differences.items(): if (param := params.get(key, None)) is not None: - # Copy the parameter to new name then delete it, to prevent Ollama warning of invalid option provided + # Move this parameter to the payload root so it is not also sent as a generation option. params[value] = params[key] del params[key] @@ -292,7 +292,7 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict: form_data[key] = value(param) del params[key] - # Unlike OpenAI, Ollama does not support params directly in the body + # Map the remaining generation parameters into options; root-level parameters were handled above. form_data['options'] = apply_model_params_to_body(params, (form_data.get('options', {}) or {}), mappings) return form_data @@ -301,7 +301,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: ollama_messages = [] for message in messages: - # Initialize the new message structure with the role + # New envelope, same speaker: carry the role across first. new_message = {'role': message['role']} # Preserve Ollama-native 'thinking' field (used by reasoning models, @@ -315,12 +315,12 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: tool_calls = message.get('tool_calls', None) tool_call_id = message.get('tool_call_id', None) - # Check if the content is a string (just a simple message) + # Plain text without tool calls gets the express lane. if isinstance(content, str) and not tool_calls: # If the content is a string, it's pure text new_message['content'] = content - # If message is a tool call, add the tool call id to the message + # Preserve the tool response's link to the call it answers. if tool_call_id: new_message['tool_call_id'] = tool_call_id @@ -339,7 +339,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: ollama_tool_calls.append(ollama_tool_call) new_message['tool_calls'] = ollama_tool_calls - # Put the content to empty string (Ollama requires an empty string for tool calls) + # Tool calls get an empty content string; Ollama insists on the blank name tag. new_message['content'] = '' else: @@ -357,7 +357,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: elif item.get('type') == 'image_url': img_url = item.get('image_url', {}).get('url', '') if img_url: - # If the image url starts with data:, it's a base64 image and should be trimmed + # Peel the data-URL label off the base64 image before packing it for Ollama. if img_url.startswith('data:'): img_url = img_url.split(',')[-1] images.append(img_url) @@ -370,7 +370,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]: if images: new_message['images'] = images - # Append the new formatted message to the result + # Add the repacked message to the outgoing mail. ollama_messages.append(new_message) return ollama_messages @@ -425,7 +425,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict: # Ollama's options field can contain parameters that should be at the root level. for key, value in ollama_root_params.items(): if (param := ollama_options.get(key, None)) is not None: - # Copy the parameter to new name then delete it, to prevent Ollama warning of invalid option provided + # Move this parameter to the payload root so it is not also sent as a generation option. ollama_payload[key] = value(param) del ollama_options[key] @@ -434,8 +434,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict: ollama_options['num_predict'] = ollama_options['max_tokens'] del ollama_options['max_tokens'] - # Ollama lacks a "system" prompt option. It has to be provided as a direct parameter, so we copy it down. - # Comment: Not sure why this is needed, but we'll keep it for compatibility. + # Preserve legacy options.system input by moving it to the payload root. if 'system' in ollama_options: ollama_payload['system'] = ollama_options['system'] del ollama_options['system'] diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index ad60c583a4..7a7bea81a4 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -203,8 +203,8 @@ def replace_imports(content): return content -# May the intent of the one who wrote it survive every -# import and transformation, as a deed survives the generations. +# Let the care put into this work survive every +# import and transformation, and serve those who carry it forward. async def load_tool_module_by_id(tool_id, content=None): """Prepare a Tool instance, frontmatter, and Python module without changing the live cache.""" if not ENABLE_TOOLS: @@ -229,8 +229,7 @@ async def load_tool_module_by_id(tool_id, content=None): module = types.ModuleType(module_name) sys.modules[module_name] = module - # Create a temporary file and use it to define `__file__` so - # that it works as expected from the module's perspective. + # Give the module a real __file__; even dynamically loaded code wants an address. temp_file = tempfile.NamedTemporaryFile(delete=False) temp_file.close() try: @@ -244,7 +243,7 @@ async def load_tool_module_by_id(tool_id, content=None): frontmatter = extract_frontmatter(content) log.info('Loaded module: %s', module.__name__) - # Create and return the object if the class 'Tools' is found in the module + # Look for the Tools class; the toolkit still needs someone to answer the door. if hasattr(module, 'Tools'): return module.Tools(), frontmatter, module else: @@ -278,8 +277,7 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non module = types.ModuleType(module_name) sys.modules[module_name] = module - # Create a temporary file and use it to define `__file__` so - # that it works as expected from the module's perspective. + # Give the module a real __file__; even dynamically loaded code wants an address. temp_file = tempfile.NamedTemporaryFile(delete=False) temp_file.close() try: diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index a4f91fc619..e2ad336c3e 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -113,9 +113,7 @@ def get_redis_client(async_mode: bool = False) -> Any | None: return None -# --------------------------------------------------------------------------- # Sentinel proxy with automatic failover retry -# --------------------------------------------------------------------------- class SentinelRedisProxy: @@ -178,7 +176,7 @@ class SentinelRedisProxy: exc, ) - # -- async wrappers ----------------------------------------------------- + # Async wrappers def _wrap_async(self, name: str, attr: Any) -> Any: if inspect.isasyncgenfunction(attr): @@ -233,7 +231,7 @@ class SentinelRedisProxy: return wrapper - # -- sync wrapper ------------------------------------------------------- + # Sync wrapper def _wrap_sync(self, name: str) -> Any: proxy = self @@ -256,9 +254,7 @@ class SentinelRedisProxy: return wrapper -# --------------------------------------------------------------------------- # Connection factory -# --------------------------------------------------------------------------- def _socket_options() -> dict[str, Any]: diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py index 66f4fc32c0..3c6bfae69f 100644 --- a/backend/open_webui/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -15,9 +15,8 @@ log = logging.getLogger(__name__) # Let the right tool be given for the work at hand, # not the one that flatters, but the one that serves. def get_task_model_id(default_model_id: str, task_model: str, task_model_external: str, models) -> str: - # Set the task model task_model_id = default_model_id - # Check if the user has a custom task model and use that model + # Use an available task model override for the local or external connection. if models.get(task_model_id, {}).get('connection_type') == 'local': if task_model and task_model in models: task_model_id = task_model @@ -85,7 +84,7 @@ async def prompt_template(template: str, user: Optional[Any] = None) -> str: # Get the current date current_date = datetime.now() - # Format the date to YYYY-MM-DD + # YYYY-MM-DD: one date format, fewer international incidents. formatted_date = current_date.strftime('%Y-%m-%d') formatted_time = current_date.strftime('%I:%M:%S %p') formatted_weekday = current_date.strftime('%A') diff --git a/backend/open_webui/utils/telemetry/instrumentors.py b/backend/open_webui/utils/telemetry/instrumentors.py index 3f72339a9d..41b460d725 100644 --- a/backend/open_webui/utils/telemetry/instrumentors.py +++ b/backend/open_webui/utils/telemetry/instrumentors.py @@ -63,10 +63,7 @@ def redis_request_hook(span: Span, instance: Union[Redis | RedisCluster], args, Redis Request Hook """ - # In cluster mode, the instance can be of two types: - # - redis.asyncio.cluster.RedisCluster - # - redis.cluster.RedisCluster - # Instead of checking the type, we check if the instance has a nodes_manager attribute. + # Detect both sync and async RedisCluster clients by their nodes_manager attribute. try: db = '' if hasattr(instance, 'nodes_manager'): diff --git a/backend/open_webui/utils/telemetry/metrics.py b/backend/open_webui/utils/telemetry/metrics.py index e730748489..cd810f9e89 100644 --- a/backend/open_webui/utils/telemetry/metrics.py +++ b/backend/open_webui/utils/telemetry/metrics.py @@ -55,17 +55,7 @@ from sqlalchemy.orm import Session logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# Sync DB helpers for OTel gauge callbacks -# -# The OTel Python SDK calls observable-instrument callbacks *synchronously* -# from a background collection thread — async callbacks are NOT supported -# (the SDK does not ``await`` the return value). -# -# Rather than bridging into the async event loop, we run plain synchronous -# SQL queries using the sync engine that is already available at setup time. -# This avoids any cross-thread / cross-loop concerns entirely. -# --------------------------------------------------------------------------- +# OTel gauge callbacks run synchronously on a collection thread, so use the sync DB engine. def _count_total_users(db_engine: Engine) -> Optional[int]: @@ -168,10 +158,7 @@ def setup_metrics(app: FastAPI, resource: Resource, db_engine: Engine) -> None: unit='ms', ) - # -- Observable gauge callbacks ---------------------------------------- - # These are called synchronously by the OTel SDK from a background - # collection thread. They use the sync DB engine directly — no async - # bridging required. + # Gauge callbacks use the sync DB engine on OTel's collection thread. def observe_total_users( options: metrics.CallbackOptions, diff --git a/backend/open_webui/utils/validate.py b/backend/open_webui/utils/validate.py index 41c7f67b59..f1fcab896c 100644 --- a/backend/open_webui/utils/validate.py +++ b/backend/open_webui/utils/validate.py @@ -61,12 +61,12 @@ def validate_image_url(url: str, *, file_only: bool = False) -> str: return url raise ValueError('Invalid background image URL: must reference an internal file.') - # --- Relative paths (exact match + anchored regex only) ----------- + # Relative paths (exact match + anchored regex only) if not url or url in _SAFE_STATIC_PATHS or _USER_PROFILE_IMAGE_RE.match(url): return url - # --- Absolute URLs ------------------------------------------------- + # Absolute URLs # urlparse normalises the scheme to lowercase, giving us # case-insensitive scheme matching for free. diff --git a/backend/open_webui/utils/webhook.py b/backend/open_webui/utils/webhook.py index a2f099af74..fbd7838935 100644 --- a/backend/open_webui/utils/webhook.py +++ b/backend/open_webui/utils/webhook.py @@ -40,14 +40,14 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict, desc try: payload = {} - # Slack and Google Chat Webhooks + # Slack and Google Chat both order their message under "text". if 'https://hooks.slack.com' in url or 'https://chat.googleapis.com' in url: payload['text'] = _event_text(message, description, event_data) - # Discord Webhooks + # Discord calls it "content" and keeps the speech under 2,000 characters. elif 'https://discord.com/api/webhooks' in url: content = _event_text(message, description, event_data) payload['content'] = content if len(content) < 2000 else f'{content[: 2000 - 20]}... (truncated)' - # Microsoft Teams Webhooks + # Teams prefers a MessageCard; apparently the message needs stationery. elif 'webhook.office.com' in url: action = event_data.get('action', 'undefined') user_data = event_data.get('user') or event_data.get('actor') or {} @@ -79,7 +79,7 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict, desc } ], } - # Default Payload + # Everyone else gets the event data without gift wrapping. else: payload = event_data diff --git a/backend/start.sh b/backend/start.sh index adf521811b..813058ef12 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -1,11 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -# --------------------------------------------------------------------------- # Container entry point for Open WebUI. # Handles secret key generation, optional Ollama/CUDA/Playwright setup, # HuggingFace Space deployment, and launches the uvicorn server. -# --------------------------------------------------------------------------- # Default optional env vars that we test below with bash's `,,` lowercase # expansion. The two can't be combined inline (`${VAR:-default,,}` makes @@ -16,7 +14,7 @@ set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) cd "$SCRIPT_DIR" || exit 1 -# ── Playwright browser installation (if configured) ────────────────────────── +# Playwright browser installation (if configured) if [[ "${USE_SLIM_DOCKER,,}" != "true" && "${WEB_LOADER_ENGINE,,}" == "playwright" ]]; then if [[ -z "${PLAYWRIGHT_WS_URL:-}" ]]; then @@ -26,7 +24,7 @@ if [[ "${USE_SLIM_DOCKER,,}" != "true" && "${WEB_LOADER_ENGINE,,}" == "playwrigh fi fi -# ── Secret key setup ───────────────────────────────────────────────────────── +# Secret key setup KEY_FILE="${WEBUI_SECRET_KEY_FILE:-.webui_secret_key}" WEBUI_SECRET_KEY_LENGTH="${WEBUI_SECRET_KEY_LENGTH:-24}" @@ -49,21 +47,21 @@ if [[ -z "${WEBUI_SECRET_KEY:-}" && -z "${WEBUI_JWT_SECRET_KEY:-}" ]]; then WEBUI_SECRET_KEY=$(cat "$KEY_FILE") fi -# ── Ollama (bundled Docker image) ──────────────────────────────────────────── +# Ollama (bundled docker image) if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then echo "Starting bundled ollama serve..." ollama serve & fi -# ── CUDA library paths ────────────────────────────────────────────────────── +# Cuda library paths if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then echo "CUDA enabled — extending LD_LIBRARY_PATH for torch/cudnn libraries." export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:/usr/local/lib/python3.12/site-packages/torch/lib:/usr/local/lib/python3.12/site-packages/nvidia/cudnn/lib" fi -# ── HuggingFace Space deployment ───────────────────────────────────────────── +# HuggingFace space deployment if [[ -n "${SPACE_ID:-}" ]]; then echo "Configuring for HuggingFace Space deployment..." @@ -93,7 +91,7 @@ if [[ -n "${SPACE_ID:-}" ]]; then export WEBUI_URL="${SPACE_HOST}" fi -# ── Launch uvicorn ─────────────────────────────────────────────────────────── +# Launch uvicorn PYTHON_CMD=$(command -v python3 || command -v python) UVICORN_WORKERS="${UVICORN_WORKERS:-1}" diff --git a/docker-cleanup.sh b/docker-cleanup.sh index 5092b31c10..be994d1ec5 100755 --- a/docker-cleanup.sh +++ b/docker-cleanup.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# --------------------------------------------------------------------------- # Tear down the compose project and remove all volumes (including data). -# --------------------------------------------------------------------------- echo "WARNING: This will stop all containers and delete all volumes (including persistent data)." read -rp "Are you sure you want to continue? [y/N]: " answer diff --git a/docker-compose-launcher.sh b/docker-compose-launcher.sh index 026de313fa..3b5c730144 100755 --- a/docker-compose-launcher.sh +++ b/docker-compose-launcher.sh @@ -1,10 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# --------------------------------------------------------------------------- # Interactive docker compose launcher for Open WebUI. # Supports GPU auto-detection, configurable ports, data mounts, and Playwright. -# --------------------------------------------------------------------------- readonly BOLD='\033[1m' readonly GREEN='\033[1;32m' @@ -13,7 +11,7 @@ readonly RED='\033[0;31m' readonly RESET='\033[0m' readonly CHECK_MARK='\xE2\x9C\x93' -# ── GPU detection ───────────────────────────────────────────────────────────── +# Gpu detection detect_gpu_driver() { if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then @@ -32,7 +30,7 @@ detect_gpu_driver() { fi } -# ── Argument helpers ────────────────────────────────────────────────────────── +# Argument helpers extract_bracket_value() { local input="$1" fallback="${2:-}" @@ -66,7 +64,7 @@ Examples: EOF } -# ── Defaults ────────────────────────────────────────────────────────────────── +# Defaults enable_gpu=false enable_api=false @@ -79,7 +77,7 @@ api_port=11435 webui_port=3000 data_dir="" -# ── Parse arguments ─────────────────────────────────────────────────────────── +# Parse arguments while [[ $# -gt 0 ]]; do case "$1" in @@ -97,7 +95,7 @@ while [[ $# -gt 0 ]]; do shift done -# ── Drop mode ───────────────────────────────────────────────────────────────── +# Drop mode if [[ "$drop_project" == true ]]; then docker compose down --remove-orphans @@ -105,7 +103,7 @@ if [[ "$drop_project" == true ]]; then exit 0 fi -# ── Build compose command ───────────────────────────────────────────────────── +# Build compose command compose_files=("-f" "docker-compose.yaml") @@ -141,7 +139,7 @@ if [[ "$build_image" == true ]]; then up_args+=("--build") fi -# ── Confirmation ────────────────────────────────────────────────────────────── +# Confirmation echo echo -e "${WHITE}${BOLD}Current Setup:${RESET}" @@ -161,7 +159,7 @@ if [[ "$headless" != true ]]; then fi fi -# ── Launch ──────────────────────────────────────────────────────────────────── +# Launch if docker compose "${compose_files[@]}" "${up_args[@]}"; then echo diff --git a/docker-ollama.sh b/docker-ollama.sh index bf9cf6046d..bce39367dd 100755 --- a/docker-ollama.sh +++ b/docker-ollama.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# --------------------------------------------------------------------------- # Pull and run the official Ollama container with optional GPU support. -# --------------------------------------------------------------------------- readonly CONTAINER="ollama" readonly HOST_PORT="${OLLAMA_PORT:-11434}" diff --git a/docker-run.sh b/docker-run.sh index 527c8d93e2..a794997ba2 100755 --- a/docker-run.sh +++ b/docker-run.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# --------------------------------------------------------------------------- # Build and run the Open WebUI Docker container locally. -# --------------------------------------------------------------------------- readonly IMAGE="open-webui" readonly CONTAINER="open-webui" diff --git a/docker-update-models.sh b/docker-update-models.sh index 5f21715618..ed4e8c869d 100755 --- a/docker-update-models.sh +++ b/docker-update-models.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# --------------------------------------------------------------------------- # Pull the latest version of every model installed in the Ollama container. -# --------------------------------------------------------------------------- readonly CONTAINER="${OLLAMA_CONTAINER:-ollama}" diff --git a/scripts/generate-sbom.sh b/scripts/generate-sbom.sh index cffcb08d69..32e8303a22 100755 --- a/scripts/generate-sbom.sh +++ b/scripts/generate-sbom.sh @@ -60,14 +60,14 @@ generate() { WORK_DIR="$(mktemp -d)" trap 'rm -rf "$WORK_DIR"' RETURN - # --- Python: resolve all transitive deps without installing --- + # Python: resolve all transitive deps without installing dim "Resolving Python transitive deps (uv pip compile)..." uv pip compile "$ROOT_DIR/backend/requirements.txt" \ --python-version 3.12 \ --quiet \ > "$WORK_DIR/requirements-resolved.txt" 2>/dev/null - # --- JavaScript: package-lock.json is already fully resolved --- + # JavaScript: package-lock.JSON is already fully resolved if [[ -f "$ROOT_DIR/package-lock.json" ]]; then cp "$ROOT_DIR/package-lock.json" "$WORK_DIR/package-lock.json" # Syft needs package.json alongside the lockfile @@ -76,7 +76,7 @@ generate() { warn "package-lock.json not found — JS deps will be skipped" fi - # --- Scan only the resolved files --- + # Scan only the resolved files dim "Scanning resolved manifests with Syft..." syft scan "dir:$WORK_DIR" \ --output "cyclonedx-json=$OUTPUT" \ @@ -175,7 +175,7 @@ else: " } -# --- Main --- +# Main cd "$ROOT_DIR" TARGET="${1:-generate}" diff --git a/src/lib/apis/calendar/index.ts b/src/lib/apis/calendar/index.ts index 9cf979cb07..c7a9aa9775 100644 --- a/src/lib/apis/calendar/index.ts +++ b/src/lib/apis/calendar/index.ts @@ -69,7 +69,7 @@ export type CalendarForm = { access_grants?: { target_type: string; target_id: string; permission: string }[]; }; -// ── Calendars ───────────────────────────────── +// Calendars export const getCalendars = async (token: string): Promise => { let error = null; @@ -220,7 +220,7 @@ export const setDefaultCalendar = async ( return res; }; -// ── Events ───────────────────────────────── +// Events export const getCalendarEvents = async ( token: string, diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 4f73c38bfe..56d3517bbb 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -20,8 +20,8 @@ const OPENAPI_HTTP_METHODS = new Set([ 'trace' ]); -// Every request sent from here is a petition. May it reach -// the one for whom it was intended, and return answered. +// Let every request reach its destination, +// and let those who wait receive an answer worthy of their patience. export const getModels = async ( token: string = '', connections: object | null = null, diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts index c6835ff208..132569371d 100644 --- a/src/lib/apis/knowledge/index.ts +++ b/src/lib/apis/knowledge/index.ts @@ -1038,7 +1038,7 @@ export const exportKnowledgeById = async (token: string, id: string) => { return res; }; -// ── Directory API ─────────────────────────────────────────────────── +// Directory API export const createKnowledgeDirectory = async ( token: string, diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 200945ce4d..6ce2ea4c14 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -511,16 +511,3 @@ export const uploadModel = async (token: string, file: File, urlIdx: string | nu } return res; }; - -// export const pullModel = async (token: string, tagName: string) => { -// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, { -// method: 'POST', -// headers: { -// 'Content-Type': 'text/event-stream', -// Authorization: `Bearer ${token}` -// }, -// body: JSON.stringify({ -// name: tagName -// }) -// }); -// }; diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts index 9f1ae87004..05e431d811 100644 --- a/src/lib/apis/prompts/index.ts +++ b/src/lib/apis/prompts/index.ts @@ -461,9 +461,7 @@ export const updatePromptAccessGrants = async ( return res; }; -//////////////////////////// // Prompt History APIs -//////////////////////////// export const getPromptHistory = async ( token: string, diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index 9d9d7cbe3f..f0c0d6f25b 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -722,9 +722,7 @@ export const getPortProxyUrl = (baseUrl: string, port: number, path: string = '' return `${baseUrl.replace(/\/$/, '')}/proxy/${port}/${path}`; }; -// --------------------------------------------------------------------------- // Notebook execution -// --------------------------------------------------------------------------- export const createNotebookSession = async ( baseUrl: string, diff --git a/src/lib/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index bf999eb00a..ee49a18dbb 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -68,11 +68,7 @@ selectedFeedback = null; }; - ////////////////////// - // // CRUD operations - // - ////////////////////// const getFeedbacks = async () => { try { diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index d05c30a4dd..a553569cfd 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2570,9 +2570,7 @@ artifactContents.set(contents); }; - ////////////////////////// // Web functions - ////////////////////////// const openCallOverlay = () => { if (isShared) return; @@ -3097,12 +3095,7 @@ }); lastScrollTop = messagesContainerElement.scrollTop; - // content-visibility: auto causes the initial scrollHeight to be based on - // estimated sizes (contain-intrinsic-size). After we scroll, previously - // off-screen messages become visible and the browser resolves their actual - // heights, which shifts scrollHeight. Re-layouts can cascade across frames - // (new sizes reveal more content, triggering further size resolution), so - // we re-scroll across two animation frames to land at the true bottom. + // Re-scroll over two frames as content-visibility resolves estimated message heights. requestAnimationFrame(() => { if (messagesContainerElement && autoScroll) { messagesContainerElement.scrollTo({ @@ -3623,9 +3616,7 @@ bridge?.update(); }; - ////////////////////////// // Chat functions - ////////////////////////// const submitPrompt = async ( inputContent: string, @@ -3642,7 +3633,7 @@ ) ); chatFiles = chatFiles.filter( - // Remove duplicates + // One copy per attachment; the model does not need an encore. (item, index, array) => array.findIndex((i) => equal(i, item)) === index ); @@ -4094,11 +4085,7 @@ history.messages[parentId].models = [...selectedModelIds]; } - // Create response messages for each selected model - // Build message_ids list: [{model_id, message_id, modelIdx}, ...] - // Uses an array instead of a dict to support duplicate model IDs in side-by-side chat. - // modelIdx identifies each side-by-side column so the backend can persist it; without - // it, duplicate models collapse into one another when the chat is reloaded. + // Use a message_ids array with modelIdx to preserve separate columns for duplicate models. const messageIdsList: Array<{ model_id: string; message_id: string; modelIdx: number }> = []; for (const [_modelIdx, modelId] of selectedModelIds.entries()) { const model = $models.filter((m) => m.id === modelId).at(0); @@ -4190,10 +4177,7 @@ primaryResponseMessageId, _chatId, { - // Always forward the message_ids list (not just for multi-model sends) so the - // backend persists each response's modelIdx — including single-column - // regenerations in a duplicate-model chat, which would otherwise lose their - // column identity and collapse on reload. + // Send message_ids even for single-column regenerations to preserve modelIdx on reload. messageIdsList: messageIdsList.length > 0 ? messageIdsList : undefined, regenerationPrompt, regenerationMessageId @@ -4298,7 +4282,7 @@ (item.type === 'file' && !isRasterImageContentType(item?.content_type)) ) ); - // Remove duplicates + // One copy per attachment; the model does not need an encore. files = files.filter((item, index, array) => array.findIndex((i) => equal(i, item)) === index); scrollToBottom(); @@ -4545,10 +4529,7 @@ onToolCallResolved(res); } - // Finish new-chat setup even if an event already supplied the chat id. - // Only update if the user hasn't navigated to a different chat - // while the request was in flight (prevents overwriting $chatId - // and causing spurious toast notifications / state duplication). + // Finish new-chat setup only if navigation has not changed the active chat during the request. if ( res.chat_id && history.messages[responseMessageId] && @@ -4587,10 +4568,7 @@ window.history.replaceState(window.history.state, '', `/c/${res.chat_id}`); await refreshChatList(localStorage.token); - // Persist chat-level params (system prompt, advanced - // params) that the backend doesn't receive in the - // chat completion request. Files are now persisted - // by the backend at chat creation time. + // Save chat params omitted from the completion request; the backend already saves files. if (Object.keys(params).length > 0) { await updateChatParams(localStorage.token, res.chat_id, params); } diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index beba0770bb..e75de6c61b 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -268,7 +268,7 @@ await tick(); }; - // ── Sessions panel state ──────────────────────────────────────────── + // Sessions panel state let terminalExpanded = false; let hasSessions = false; let terminalHeight = 200; // px, default when expanded @@ -326,7 +326,7 @@ window.addEventListener('mouseup', onMouseUp); }; - // ── Directory state ────────────────────────────────────────────────── + // Directory state let currentPath = savedPath; let fileRoot: TerminalFileRoot | null = savedFileRoot; let entries: FileEntry[] = []; @@ -334,7 +334,7 @@ let loading = false; let error: string | null = null; - // ── Browser view state ────────────────────────────────────────────── + // Browser view state type SortMode = 'name' | 'size' | 'date' | 'created'; type BrowserRow = FileEntry & { fullPath: string; @@ -504,7 +504,7 @@ localStorage.setItem('fileNav:viewMode', mode); }; - // ── Navigation history ────────────────────────────────────────────── + // Navigation history type NavEntry = { path: string; file: string | null }; let navHistory: NavEntry[] = []; let navIndex = -1; @@ -552,7 +552,7 @@ navigatingHistory = false; }; - // ── File preview state ─────────────────────────────────────────────── + // File preview state let selectedFile: string | null = null; let showGitDiff = false; let gitFileKey = ''; @@ -582,7 +582,7 @@ let fileSearchTarget: FileSearchTarget | null = null; let documentTargetPage: number | null = null; - // ── Office preview state ──────────────────────────────────────────── + // Office preview state let fileOfficeHtml: string | null = null; let fileOfficeSlides: string[] | null = null; let currentSlide = 0; @@ -590,7 +590,7 @@ let selectedExcelSheet = ''; let excelWorkbook: import('xlsx').WorkBook | null = null; - // ── File preview toolbar state (bound from FilePreview) ───────────── + // File preview toolbar state (bound from FilePreview) let editing = false; let showRaw = false; let saving = false; @@ -612,7 +612,7 @@ $: isTextFile = fileContent !== null && fileImageUrl === null && filePdfData === null && !isOfficeFile; - // ── Upload / folder creation ───────────────────────────────────────── + // Upload / folder creation let isDragOver = false; let uploading = false; let creatingFolder = false; @@ -623,12 +623,12 @@ let newFileInput: HTMLInputElement; let directoryUploadInput: HTMLInputElement; - // ── Delete confirmation ────────────────────────────────────────────── + // Delete confirmation let deleteTarget: { path: string; name: string } | null = null; let showDeleteConfirm = false; let shiftKey = false; - // ── Terminal resolution ────────────────────────────────────────────── + // Terminal resolution let selectedTerminal: { url: string; key: string } | null = null; let terminalChatContextPending = false; let terminalChatContextHidden = false; @@ -721,7 +721,7 @@ } } - // ── Helpers ────────────────────────────────────────────────────────── + // Helpers const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'avif']); const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'ogv', 'avi', 'mkv']); const AUDIO_EXTS = new Set(['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac', 'wma', 'opus']); @@ -965,7 +965,7 @@ ); }; - // ── File preview management ────────────────────────────────────────── + // File preview management const clearFilePreview = () => { commentGeneration++; fileSearchTarget = null; @@ -995,7 +995,7 @@ excelWorkbook = null; }; - // ── Directory operations ───────────────────────────────────────────── + // Directory operations const loadDir = async ( path: string, options: { preserveTree?: boolean; restoreTree?: boolean } = {} @@ -1421,7 +1421,7 @@ } }; - // ── Drag-and-drop upload ───────────────────────────────────────────── + // Drag-and-drop upload type UploadEntry = { path: string; file?: File }; async function readDroppedFiles(data: DataTransfer): Promise { @@ -1537,7 +1537,7 @@ directoryUploadInput.click(); }; - // ── Folder creation ────────────────────────────────────────────────── + // Folder creation const startNewFolderIn = async (path: string) => { if (await loadDir(path)) await startNewFolder(); }; @@ -1572,7 +1572,7 @@ await refreshBrowser(); }; - // ── File creation ──────────────────────────────────────────────────── + // File creation const startNewFileIn = async (path: string) => { if (await loadDir(path)) await startNewFile(); }; @@ -1609,7 +1609,7 @@ await refreshBrowser(); }; - // ── Delete ─────────────────────────────────────────────────────────── + // Delete const handleDelete = async (path: string, name: string) => { const terminal = selectedTerminal; if (!terminal || !currentWritable) return; @@ -1630,7 +1630,7 @@ showDeleteConfirm = true; }; - // ── Move (drag-and-drop) ──────────────────────────────────────────── + // Move (drag-and-drop) const sourceParentPath = (source: string) => { const cleanSource = normalizePath(source).replace(/\/$/, ''); const index = cleanSource.lastIndexOf('/'); @@ -1683,7 +1683,7 @@ } }; - // ── Rename ────────────────────────────────────────────────────────── + // Rename const handleRename = async (oldPath: string, newName: string) => { const terminal = selectedTerminal; if (!terminal || !newName || !currentWritable) return; @@ -1709,7 +1709,7 @@ await refreshBrowser(); }; - // ── Multi-select ──────────────────────────────────────────────────── + // Multi-select let selectedEntries: Set = new Set(); let lastClickedIndex: number | null = null; let selectionMode = false; @@ -1853,7 +1853,7 @@ } }; - // ── Lifecycle ──────────────────────────────────────────────────────── + // Lifecycle onMount(() => { showHidden = localStorage.getItem('fileNav:showHidden') === 'true'; viewMode = localStorage.getItem('fileNav:viewMode') === 'grid' ? 'grid' : 'list'; diff --git a/src/lib/components/chat/FileNav/FileEntryRow.svelte b/src/lib/components/chat/FileNav/FileEntryRow.svelte index 83fb7dc3f0..c304a381cb 100644 --- a/src/lib/components/chat/FileNav/FileEntryRow.svelte +++ b/src/lib/components/chat/FileNav/FileEntryRow.svelte @@ -44,7 +44,7 @@ export let onMove: (sources: string[], destFolder: string) => void | Promise = () => {}; export let onRename: (oldPath: string, newName: string) => void = () => {}; - // ── Selection ───────────────────────────────────────────────────────── + // Selection export let selected: boolean = false; export let active = false; export let selectionMode: boolean = false; @@ -148,7 +148,7 @@ }; }; - // ── Rename state ───────────────────────────────────────────────────── + // Rename state let renaming = false; let renameValue = ''; let renameInput: HTMLInputElement; @@ -179,7 +179,7 @@ renameValue = ''; }; - // ── Long-press for touch selection ─────────────────────────────────── + // Long-press for touch selection let longPressTimer: ReturnType | null = null; let didLongPress = false; @@ -213,7 +213,7 @@ clearExpandTimer(); }); - // ── Click handler ──────────────────────────────────────────────────── + // Click handler const handleClick = (e: MouseEvent) => { openPreview = false; if (renaming) return; diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte index 365ee49c8e..cbe838c033 100644 --- a/src/lib/components/chat/FileNav/FilePreview.svelte +++ b/src/lib/components/chat/FileNav/FilePreview.svelte @@ -206,7 +206,7 @@ $: csvHeader = csvRows.length > 0 ? csvRows[0] : []; $: csvBody = csvRows.length > 1 ? csvRows.slice(1) : []; - // ── Shiki code highlighting (SVG only) ────────────────────────────── + // Shiki code highlighting (SVG only) let highlightedHtml: string | null = null; let highlightingFile: string | null = null; @@ -231,7 +231,7 @@ highlightedHtml = null; } - // ── JSON parsing ──────────────────────────────────────────────────── + // JSON parsing let parsedJson: unknown = undefined; let jsonError: string | null = null; @@ -248,7 +248,7 @@ jsonError = null; } - // ── Notebook parsing ───────────────────────────────────────────────── + // Notebook parsing let parsedNotebook: Record | null = null; $: if (isNotebook && fileContent !== null) { @@ -691,7 +691,7 @@ padding-left: 1.5em; margin: 0.5em 0; } - /* ── Shiki code highlighting ─────────────────────────────────── */ + /* Shiki code highlighting */ .shiki-preview :global(pre.shiki) { margin: 0; padding: 0.75rem 1rem; diff --git a/src/lib/components/chat/FileNav/NotebookView.svelte b/src/lib/components/chat/FileNav/NotebookView.svelte index f7d658be2d..327454aae1 100644 --- a/src/lib/components/chat/FileNav/NotebookView.svelte +++ b/src/lib/components/chat/FileNav/NotebookView.svelte @@ -46,7 +46,7 @@ const toStr = (s: string[] | string | undefined): string => Array.isArray(s) ? s.join('') : (s ?? ''); - // ── Syntax highlighting ────────────────────────────────────────────── + // Syntax highlighting let highlightedCells: Record = {}; const highlightCell = async (index: number, code: string, language: string) => { @@ -73,7 +73,7 @@ }); } - // ── Markdown / output helpers ──────────────────────────────────────── + // Markdown / output helpers const renderMarkdown = (src: string): string => DOMPurify.sanitize(marked.parse(src, { async: false }) as string); @@ -102,7 +102,7 @@ return null; }; - // ── Cell editing ───────────────────────────────────────────────────── + // Cell editing let editingCell: Record = {}; let editedSources: Record = {}; @@ -120,7 +120,7 @@ highlightCell(index, toStr(cells[index].source), lang); }; - // ── Session / kernel ───────────────────────────────────────────────── + // Session / kernel let sessionId: string | null = null; let kernelReady = false; let kernelStarting = false; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c77204ca7d..4752e4eea6 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -744,16 +744,8 @@ if (isComposing) { return true; } + // On Safari, ignore one keydown within 500 ms of compositionend so confirming IME text adds no newline. // See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/. - // On Japanese input method editors (IMEs), the Enter key is used to confirm character - // selection. On Safari, when Enter is pressed, compositionend and keydown events are - // emitted. The keydown event triggers newline insertion, which we don't want. - // This method returns true if the keydown event should be ignored. - // We only ignore it once, as pressing Enter a second time *should* insert a newline. - // Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp. - // This guards against the case where compositionend is triggered without the keyboard - // (e.g. character confirmation may be done with the mouse), and keydown is triggered - // afterwards- we wouldn't want to ignore the keydown event in this case. if (isSafari && Math.abs(event.timeStamp - compositionEndedAt) < 500) { compositionEndedAt = -2e8; return true; @@ -977,19 +969,19 @@ video: { cursor: 'never' }, audio: false }); - // Once the user selects a screen, temporarily create a video element + // Borrow a video element for the selected screen; this is a one-frame production. const video = document.createElement('video'); video.srcObject = mediaStream; // Ensure the video loads without affecting user experience or tab switching await video.play(); - // Set up the canvas to match the video dimensions + // Match the canvas to the video; no surprise cropping in this photo booth. const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; - // Grab a single frame from the video stream using the canvas + // One frame, please. We are taking a screenshot, not directing a film. const context = canvas.getContext('2d'); context.drawImage(video, 0, 0, canvas.width, canvas.height); - // Stop all video tracks (stop screen sharing) after capturing the image + // Screenshot captured. Stop the tracks before this becomes a live broadcast. mediaStream.getTracks().forEach((track) => track.stop()); // bring back focus to this current tab, so that the user can see the screen capture @@ -1000,7 +992,7 @@ const blob = await (await fetch(imageUrl)).blob(); const file = new File([blob], `screen-capture-${Date.now()}.png`, { type: 'image/png' }); inputFilesHandler([file]); - // Clean memory: Clear video srcObject + // Release the video stream reference; the photo booth is closed. video.srcObject = null; } catch (error) { // Handle any errors (e.g., user cancels screen sharing) @@ -1355,7 +1347,7 @@ ); if (res) { - // Clear the input content saved in session storage. + // The draft has become a note; session storage can stop keeping a spare. sessionStorage.removeItem('chat-input'); goto(`/notes/${res.id}`); } @@ -1394,7 +1386,7 @@ try { const data = JSON.parse(textData); if (data.type === 'chat' && data.id) { - // Fetch the chat to get its title, then add as a reference chat + // Fetch the chat title so the attachment arrives with a name tag. const chat = await getChatById(localStorage.token, data.id); if (chat) { const chatItem = { @@ -1412,7 +1404,7 @@ e.stopPropagation(); return; } else if (data.type === 'folder' && data.id) { - // Fetch the folder to get its name, then add as a reference folder + // Fetch the folder name; an ID alone makes a poor introduction. const folder = await getFolderById(localStorage.token, data.id); if (folder) { const folderItem = { @@ -1429,7 +1421,7 @@ e.stopPropagation(); return; } else if (data.type === 'note' && data.id) { - // Fetch the note to get its title, then add as a reference note + // Fetch the note title before introducing it to the conversation. const note = await getNoteById(localStorage.token, data.id); if (note) { const noteItem = { diff --git a/src/lib/components/chat/MessageInput/CallOverlay.svelte b/src/lib/components/chat/MessageInput/CallOverlay.svelte index 1bc4157635..a674177ade 100644 --- a/src/lib/components/chat/MessageInput/CallOverlay.svelte +++ b/src/lib/components/chat/MessageInput/CallOverlay.svelte @@ -442,7 +442,7 @@ // Check if initial speech/noise has started const hasSound = domainData.some((value) => value > 0); if (hasSound) { - // BIG RED TEXT + // Big red text console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected'); if (mediaRecorder && mediaRecorder.state !== 'recording') { mediaRecorder.start(); @@ -750,10 +750,7 @@ const chatEventHandler = async (e) => { const { id, content } = e.detail; - // "id" here is message id - // if "id" is not the same as "currentMessageId" then do not process - // "content" here is a sentence from the assistant, - // there will be many sentences for the same "id" + // Collect assistant sentences only for the active message ID. if (currentMessageId === id) { console.log(`Received chat event for message ID ${id}: ${content}`); diff --git a/src/lib/components/chat/MessageInput/VoiceRecording.svelte b/src/lib/components/chat/MessageInput/VoiceRecording.svelte index ff6158fde2..94d80d766a 100644 --- a/src/lib/components/chat/MessageInput/VoiceRecording.svelte +++ b/src/lib/components/chat/MessageInput/VoiceRecording.svelte @@ -162,11 +162,6 @@ // lastSoundTime = Date.now(); // } - // if (recording && Date.now() - lastSoundTime > 3000) { - // if ($settings?.speechAutoSend ?? false) { - // confirmRecording(); - // } - // } } window.requestAnimationFrame(processFrame); diff --git a/src/lib/components/chat/Messages/OutputEditView.svelte b/src/lib/components/chat/Messages/OutputEditView.svelte index fc022f4710..2fd8b07117 100644 --- a/src/lib/components/chat/Messages/OutputEditView.svelte +++ b/src/lib/components/chat/Messages/OutputEditView.svelte @@ -21,7 +21,7 @@ let viewMode: 'visual' | 'json' = 'visual'; let jsonError = ''; - // --- CodeMirror --- + // CodeMirror let cmContainer: HTMLDivElement; let cmEditor: EditorView | null = null; let editorTheme = new Compartment(); @@ -87,7 +87,7 @@ onDestroy(() => destroyCodeMirror()); - // --- Display items --- + // Display items interface DisplayItem { type: 'message' | 'reasoning' | 'function_call' | 'code_interpreter' | 'openai_tool'; @@ -134,7 +134,7 @@ $: displayItems = buildDisplayItems(output); - // --- Helpers --- + // Helpers function getMessageText(item: any): string { return (item.content ?? []) diff --git a/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte b/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte index da73558eb9..a5c4e5a79f 100644 --- a/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte @@ -45,7 +45,7 @@ - + - + - + IMAGE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? ''); - // ── Worker management ───────────────────────────────────────────────── + // Worker management function ensureWorker(): Worker { let worker = $pyodideWorker; @@ -127,7 +127,7 @@ }); } - // ── Breadcrumbs ─────────────────────────────────────────────────────── + // Breadcrumbs const buildBreadcrumbs = (path: string) => { const parts = path.split('/').filter(Boolean); @@ -143,7 +143,7 @@ $: breadcrumbs = buildBreadcrumbs(currentPath); - // ── Operations ──────────────────────────────────────────────────────── + // Operations const loadDir = async (path: string) => { loading = true; @@ -309,7 +309,7 @@ await loadDir(currentPath); }; - // ── Drag and drop ───────────────────────────────────────────────────── + // Drag and drop const handleDragOver = (e: DragEvent) => { if (selectedFile) return; @@ -328,7 +328,7 @@ if (droppedFiles.length) await uploadFiles(droppedFiles); }; - // ── Lifecycle ───────────────────────────────────────────────────────── + // Lifecycle const onFilesChanged = async () => { try { diff --git a/src/lib/components/chat/Settings/Audio.svelte b/src/lib/components/chat/Settings/Audio.svelte index d15132cac9..d95c1756ea 100644 --- a/src/lib/components/chat/Settings/Audio.svelte +++ b/src/lib/components/chat/Settings/Audio.svelte @@ -145,10 +145,6 @@ await getVoices(); - // const rawAudio = await tts.generate(inputText, { - // // Use `tts.list_voices()` to list all available voices - // voice: voice - // }); // const blobUrl = URL.createObjectURL(await rawAudio.toBlob()); // const audio = new Audio(blobUrl); diff --git a/src/lib/components/common/Banner.svelte b/src/lib/components/common/Banner.svelte index 420490fd62..fd9ffc23ca 100644 --- a/src/lib/components/common/Banner.svelte +++ b/src/lib/components/common/Banner.svelte @@ -82,7 +82,7 @@ - + - + { if (node.scrollHeight === lastContentHeight) return; lastContentHeight = node.scrollHeight; diff --git a/src/lib/components/common/FullHeightIframe.svelte b/src/lib/components/common/FullHeightIframe.svelte index d75d3da7a3..317ef5fa21 100644 --- a/src/lib/components/common/FullHeightIframe.svelte +++ b/src/lib/components/common/FullHeightIframe.svelte @@ -93,7 +93,7 @@ const scriptTags: string[] = []; - // --- Alpine.js detection & injection --- + // Alpine.js detection & injection const hasAlpineDirectives = alpineDirectives.some((dir) => html.includes(dir)); if (hasAlpineDirectives) { try { @@ -107,7 +107,7 @@ } } - // --- Chart.js detection & injection --- + // Chart.js detection & injection const chartJsDirectives = ['new Chart(', 'Chart.']; const hasChartJsDirectives = chartJsDirectives.some((dir) => html.includes(dir)); if (hasChartJsDirectives) { diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index af43a4f809..078eceef39 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -40,11 +40,7 @@ }); turndownService.escape = (string) => string; - // Produce single newlines between paragraphs instead of double. - // TipTap wraps every line in tags; the default Turndown rule emits - // \n\n around each paragraph which then required a destructive - // replaceAll('\n\n','\n') that also wiped blank lines inside code blocks. - // This rule eliminates that hack so content is untouched. + // Emit one newline per paragraph; global newline collapsing would damage code-block spacing. turndownService.addRule('singleNewlineParagraphs', { filter: 'p', replacement: function (content) { @@ -55,7 +51,7 @@ // Use turndown-plugin-gfm for proper GFM table support turndownService.use(gfm); - // Add custom table header rule before using GFM plugin + // Keep header text intact; the table rule will handle the scaffolding. turndownService.addRule('tableHeaders', { filter: 'th', replacement: function (content, node) { @@ -63,7 +59,7 @@ } }); - // Add custom table rule to handle headers properly + // Rebuild tables as Markdown; the columns still need to stay in their lanes. turndownService.addRule('tables', { filter: 'table', replacement: function (content, node) { @@ -78,7 +74,7 @@ const cellContents = cells.map((cell) => { // Get the text content and clean it up let cellContent = turndownService.turndown(cell.innerHTML).trim(); - // Remove extra paragraph tags that might be added + // Trim edge newlines; a table cell does not need its own front lawn. cellContent = cellContent.replace(/^\n+|\n+$/g, ''); cellContent = cellContent.replace(/\n/g, ''); return cellContent; @@ -87,7 +83,7 @@ // Add the row markdown += '| ' + cellContents.join(' | ') + ' |\n'; - // Add separator after first row (which should be headers) + // Give the first row its Markdown header underline. Tiny table bureaucracy. if (rowIndex === 0) { const separator = cells.map(() => '---').join(' | '); markdown += '| ' + separator + ' |\n'; @@ -241,10 +237,7 @@ } }); - // WORKAROUND: TipTap's default Code mark input rule regex captures the - // character before the opening backtick, causing it to be deleted. - // This uses a lookbehind assertion instead so the preceding character is - // matched for position but not captured/deleted. + // Use lookbehind so the backtick rule does not capture and delete the preceding character. // Upstream fix: https://github.com/ueberdosis/tiptap/pull/7124 const backtickInputRegex = /(?<=\s|^)`([^`]+)`(?!`)$/; const FixedCode = Code.extend({ @@ -566,20 +559,20 @@ .trim() ); - // Create a temporary div to parse HTML + // Give the sanitized HTML a temporary home while the parser unpacks it. const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; // Convert HTML to ProseMirror nodes const fragment = DOMParser.fromSchema(state.schema).parse(tempDiv); - // Extract just the content, not the wrapper paragraphs + // Unpack the fragment; paragraph wrappers come off in the loop below. const content = fragment.content; let nodesToInsert = []; content.forEach((node) => { if (node.type.name === 'paragraph') { - // If it's a paragraph, extract its content + // Keep the paragraph contents; leave the packing material behind. nodesToInsert.push(...node.content.content); } else { nodesToInsert.push(node); @@ -587,12 +580,12 @@ }); tr = tr.replaceWith(start, end, nodesToInsert); - // Calculate new position + // Park the caret after the inserted nodes, not back at the loading dock. const newPos = start + nodesToInsert.reduce((sum, node) => sum + node.nodeSize, 0); tr = tr.setSelection(Selection.near(tr.doc.resolve(newPos))); } else { if (text.includes('\n')) { - // Split the text into lines and create a node for each line + // Keep the first line inline and wrap subsequent lines in paragraphs. const lines = text.split('\n'); const nodes = lines.map( (line, index) => @@ -601,17 +594,17 @@ : state.schema.nodes.paragraph.create({}, line ? state.schema.text(line) : undefined) // Subsequent lines are paragraphs ); - // Build and dispatch the transaction to replace the word at cursor + // Replace the current word in the pending transaction; dispatch after setting the selection. tr = tr.replaceWith(start, end, nodes); let newSelectionPos; - // +1 because the insert happens at start, so last para starts at (start + sum of all previous nodes' sizes) + // Sum node sizes from the insertion point to locate the end of the inserted content. let lastPos = start; for (let i = 0; i < nodes.length; i++) { lastPos += nodes[i].nodeSize; } - // Place cursor inside the last paragraph at its end + // Use that boundary to find the nearest valid text selection below. newSelectionPos = lastPos; tr = tr.setSelection(TextSelection.near(tr.doc.resolve(newSelectionPos))); @@ -758,7 +751,7 @@ export const focus = (options: FocusOptions = {}) => { if (editor && editor.view) { - // Check if the editor is destroyed + // Retired editors do not take focus requests. if (editor.isDestroyed) { return; } @@ -768,7 +761,7 @@ editor.view.dom.focus(options); } else { editor.view.focus(); - // Scroll to the current selection + // Bring the selection into view; the caret should not play hide-and-seek. editor.view.dispatch(editor.view.state.tr.scrollIntoView()); } } catch (e) { diff --git a/src/lib/components/common/RichTextInput/AutoCompletion.js b/src/lib/components/common/RichTextInput/AutoCompletion.js index 025ee554cc..b8828157d1 100644 --- a/src/lib/components/common/RichTextInput/AutoCompletion.js +++ b/src/lib/components/common/RichTextInput/AutoCompletion.js @@ -187,26 +187,6 @@ export const AIAutocompletion = Extension.create({ if (event.key === 'Tab') { if (acceptFollowUpSuggestion(view)) return true; - // if (!node.attrs['data-suggestion']) { - // // Generate completion - // if (loading) return true - // loading = true - // const prompt = node.textContent - // this.options.generateCompletion(prompt).then(suggestion => { - // if (suggestion && suggestion.trim() !== '') { - // dispatch(state.tr.setNodeMarkup($head.before(), null, { - // ...node.attrs, - // class: 'ai-autocompletion', - // 'data-prompt': prompt, - // 'data-suggestion': suggestion, - // })) - // } - // // If suggestion is empty or null, do nothing - // }).finally(() => { - // loading = false - // }) - // } - if (node.attrs['data-suggestion']) { // Accept suggestion const suggestion = node.attrs['data-suggestion']; @@ -283,34 +263,6 @@ export const AIAutocompletion = Extension.create({ } return false; }, - // Add mousedown behavior - // mouseup: (view, event) => { - // const { state, dispatch } = view; - // const { selection } = state; - // const { $head } = selection; - // const node = $head.parent; - - // // Reset debounce timer on mouse click - // clearTimeout(debounceTimer); - - // // If a suggestion exists and the cursor moves, remove the suggestion - // if ( - // node.type.name === 'paragraph' && - // node.attrs['data-suggestion'] && - // view.state.selection.$head.pos !== view.state.selection.$head.end() - // ) { - // dispatch( - // state.tr.setNodeMarkup($head.before(), null, { - // ...node.attrs, - // class: null, - // 'data-prompt': null, - // 'data-suggestion': null - // }) - // ); - // } - - // return false; - // } mouseup: (view, event) => { const { state, dispatch } = view; diff --git a/src/lib/components/common/RichTextInput/listDragHandlePlugin.js b/src/lib/components/common/RichTextInput/listDragHandlePlugin.js index d0ce93f380..ac78b1ff39 100644 --- a/src/lib/components/common/RichTextInput/listDragHandlePlugin.js +++ b/src/lib/components/common/RichTextInput/listDragHandlePlugin.js @@ -192,7 +192,7 @@ export function listDragHandlePlugin(options = {}) { return wantedType.create(newAttrs, Fragment.from(normalizedContent), itemNode.marks); } - // ---------- decorations ---------- + // Decorations function buildHandleDecos(doc) { const decos = []; doc.descendants((node, pos) => { @@ -269,7 +269,7 @@ export function listDragHandlePlugin(options = {}) { return { ...li, dom, mode }; } - // ---------- state ---------- + // State const init = (state) => ({ decorations: buildHandleDecos(state.doc), dragging: null, // {fromStart, startMouse:{x,y}, ghostEl, active} @@ -312,7 +312,7 @@ export function listDragHandlePlugin(options = {}) { return deco; }; - // ---------- helpers ---------- + // Helpers const setDrag = (view, dragging) => view.dispatch(view.state.tr.setMeta(listPointerDragKey, { type: 'set-drag', dragging })); const setDrop = (view, drop) => @@ -422,7 +422,7 @@ export function listDragHandlePlugin(options = {}) { if (ghost) ghost.style.transform = `translate(${Math.round(dx)}px, ${Math.round(dy)}px)`; }; - // ---------- plugin ---------- + // Plugin return new Plugin({ key: listPointerDragKey, state: { init: (_, state) => init(state), apply }, diff --git a/src/lib/components/common/Textarea.svelte b/src/lib/components/common/Textarea.svelte index 2eb32e84cf..48a58e1e95 100644 --- a/src/lib/components/common/Textarea.svelte +++ b/src/lib/components/common/Textarea.svelte @@ -33,9 +33,7 @@ const resize = () => { if (textareaElement) { - // Find all ancestors that currently have an active scroll offset. - // This is extremely fast because reading `scrollTop` on a clean layout doesn't trigger reflow, - // and it makes the fix 100% robust without relying on hardcoded CSS classes. + // Remember scrolled ancestors before resizing; typing should not rearrange the scenery. const activeScrollParents: { el: HTMLElement; top: number }[] = []; let p = textareaElement.parentNode; while (p && p !== document.body) { @@ -58,8 +56,7 @@ textareaElement.style.height = `${height}px`; - // Only restore scroll for elements that were actually scrolled. - // This prevents layout thrashing that happens if we blindly set `.scrollTop` on every parent. + // Restore only changed offsets; the rest of the page can stay seated. activeScrollParents.forEach((p) => { if (p.el.scrollTop !== p.top) p.el.scrollTop = p.top; }); diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 21937582a9..d8576f759b 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -226,12 +226,7 @@ } }; - // Keep the optimistic sidebar highlight in sync with the active chat. Leaving the - // chat view (e.g. navigating to an admin page) clears chatId, and programmatic - // navigation such as cloning moves chatId to a different chat — in both cases the - // previously-selected item must not stay highlighted. The optimistic on-click - // highlight is preserved because a click sets selectedChatId without changing - // chatId, so this reactive only re-runs once chatId catches up to the same value. + // Follow chatId changes on navigation; preserve the click's optimistic highlight until chatId catches up. $: selectedChatId = $chatId || null; let showCreateChannel = false; @@ -410,7 +405,7 @@ ? [...folderMap[folder.parent_id].childrenIds, folder.id] : [folder.id]; - // Sort the children by updated_at field + // Recently updated folders get the front seats. folderMap[folder.parent_id].childrenIds.sort((a, b) => { return folderMap[b].updated_at - folderMap[a].updated_at; }); @@ -445,10 +440,10 @@ return; } - // Check for duplicate names in the same parent + // Check sibling names before adding another folder called "New folder". const siblings = Object.values(folders).filter((folder) => folder.parent_id === parent_id); if (siblings.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) { - // If a folder with the same name already exists, append a number to the name + // Same name? Issue a numbered sequel. let i = 1; while ( siblings.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase()) @@ -500,7 +495,7 @@ }; const initChatList = async () => { - // Reset pagination variables + // Mark the chat list as loading while tags, pinned notes, and chats refresh. console.log('initChatList'); chatListReady = false; diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 70a4fbddb6..a6dc3e5dc8 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -632,14 +632,14 @@ if (!pendingSyncFiles?.length) return; try { - // ── 2. Compute checksums ── + // 2. Compute checksums syncing = $i18n.t('Computing checksums ({{count}} files)', { count: pendingSyncFiles.length }); const manifest = await buildDirectoryManifest(pendingSyncFiles); pendingSyncFiles = null; - // ── 3. Diff against knowledge base ── + // 3. Diff against knowledge base syncing = $i18n.t('Comparing with knowledge base...'); const diff = await syncKnowledgeDiff( localStorage.token, @@ -652,7 +652,7 @@ return; } - // ── 4. Cleanup — remove deleted + stale modified files first ── + // 4. Cleanup — remove deleted + stale modified files first const staleFileIds = [ ...diff.deleted.map((d: any) => d.file_id), ...diff.modified.map((m: any) => m.stale_file_id) @@ -665,10 +665,10 @@ browser?.clearRemovedSelection(staleFileIds); } - // ── 5. mkdir — create missing directories (parents first) ── + // 5. Mkdir — create missing directories (parents first) const directoryIdByPath = await createMissingDirectories(diff); - // ── 6. Upload added + modified files ── + // 6. Upload added + modified files const filesToUpload = manifest.filter( (entry) => diff.added.some((a: any) => a.filename === entry.filename && a.path === entry.path) || @@ -679,7 +679,7 @@ entry.path ? directoryIdByPath[entry.path] : null ); - // ── 7. Report ── + // 7. Report if (failedCount === 0) { toast.success( $i18n.t( diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index e730f224fc..c7791ca92b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -15,9 +15,7 @@ import type { I18nOverrides } from '$lib/utils/translationDictionary'; import emojiShortCodes from '$lib/emoji-shortcodes.json'; -// What is held here is the only truth the house knows. -// When it changes, let every room hear at once. -// Backend +// Backend identity and session state export const WEBUI_NAME = writable(APP_NAME); export const WEBUI_VERSION = writable(null); diff --git a/src/lib/utils/codeHighlight.ts b/src/lib/utils/codeHighlight.ts index c77f717d6d..b7f065273b 100644 --- a/src/lib/utils/codeHighlight.ts +++ b/src/lib/utils/codeHighlight.ts @@ -57,10 +57,7 @@ const EXT_OVERRIDE: Record = { jsonl: 'jsonl' }; -// Common extensions that exactly match their Shiki language ID. -// This replaces the runtime `bundledLanguages` import from shiki, which -// pulled ~5-10MB of JavaScript into the initial page load just so -// isCodeFile() could check extension support. +// Keep extension IDs local to avoid loading Shiki's full language bundle just to recognize files. const KNOWN_LANG_IDS = new Set([ 'ada', 'awk', diff --git a/src/lib/utils/connections.ts b/src/lib/utils/connections.ts index f7c7a6aed7..9b4441ac9c 100644 --- a/src/lib/utils/connections.ts +++ b/src/lib/utils/connections.ts @@ -7,7 +7,7 @@ import { getOpenAIConfig, updateOpenAIConfig } from '$lib/apis/openai'; import { getTerminalServerConnections, setTerminalServerConnections } from '$lib/apis/configs'; -// ─── OpenAI Connections ───────────────────────────────── +// OpenAI connections /** * Add an OpenAI-compatible API connection at the system level. @@ -74,7 +74,7 @@ export const removeOpenAIConnection = async (token: string, url: string) => { }); }; -// ─── Terminal Server Connections ──────────────────────── +// Terminal server connections /** * Add a terminal server connection at the system level. diff --git a/src/lib/utils/hash.ts b/src/lib/utils/hash.ts index f71bc2e3ab..1b1940aaa3 100644 --- a/src/lib/utils/hash.ts +++ b/src/lib/utils/hash.ts @@ -26,7 +26,7 @@ function hexEncode(bytes: Uint8Array): string { return hex.join(''); } -// ── Pure-JS SHA-256 (RFC 6234) ────────────────────────────────────────────── +// Pure-JS SHA-256 (RFC 6234) // Used only when crypto.subtle is unavailable (HTTP deployments). const K: Uint32Array = new Uint32Array([ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index a69a505bb2..23c440d090 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -29,11 +29,9 @@ import markedKatexExtension from '$lib/utils/marked/katex-extension'; import hljs from 'highlight.js'; import { decode } from 'html-entities'; -////////////////////////// // Helper functions // No one thanks the foundation, but without it the // house falls. Let the quiet work here hold. -////////////////////////// export const resolveDefaultModelIds = ( models: { id: string; info?: { meta?: { hidden?: boolean } } }[], @@ -245,10 +243,7 @@ export const convertMessagesToHistory = (messages) => { return history; }; -// Repair structurally-malformed history nodes from failed regenerations. -// A lost assistant placeholder may exist under its map key with only -// completion fields (content/done/error), missing id/role/parentId. -// Reconstruct graph fields so already-corrupted chats recover on open. +// Rebuild missing graph fields so chats with incomplete regeneration placeholders can reopen. export const sanitizeHistory = (history) => { if (!history?.messages || typeof history.messages !== 'object') return; @@ -703,32 +698,27 @@ export const extractCurlyBraceWords = (text) => { export const removeLastWordFromString = (inputString, wordString) => { console.log('inputString', inputString); - // Split the string by newline characters to handle lines separately + // Only edit the final line; preserve earlier lines verbatim. const lines = inputString.split('\n'); - // Take the last line to operate only on it const lastLine = lines.pop(); - // Split the last line into an array of words const words = lastLine.split(' '); - // Conditional to check for the last word removal + // Remove the final token if it matches, or the escaped hash marker when wordString is empty. if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) { - words.pop(); // Remove last word if condition is satisfied + words.pop(); } - // Join the remaining words back into a string and handle space correctly let updatedLastLine = words.join(' '); - // Add a trailing space to the updated last line if there are still words + // Append a space to any nonempty result, even when no token was removed. if (updatedLastLine !== '') { updatedLastLine += ' '; } - // Combine the lines together again, placing the updated last line back in const resultString = [...lines, updatedLastLine].join('\n'); - // Return the final string console.log('resultString', resultString); return resultString; @@ -741,7 +731,7 @@ export const removeFirstHashWord = (inputString) => { // Find the index of the first word that starts with # const index = words.findIndex((word) => word.startsWith('#')); - // Remove the first word with # + // Retire the first hashtag; the rest of the sentence keeps its job. if (index !== -1) { words.splice(index, 1); } @@ -753,13 +743,13 @@ export const removeFirstHashWord = (inputString) => { }; export const transformFileName = (fileName) => { - // Convert to lowercase + // Lowercase the filename; shouting does not help us find it. const lowerCaseFileName = fileName.toLowerCase(); - // Remove special characters using regular expression + // Strip punctuation; word characters and whitespace make the guest list. const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); - // Replace spaces with dashes + // Give each whitespace run one dash, regardless of its enthusiasm. const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); return finalFileName; @@ -779,7 +769,7 @@ export const calculateSHA256 = async (file) => { reader.readAsArrayBuffer(file); try { - // Wait for the FileReader to finish reading the file + // Wait for the bytes; hashing an IOU would be optimistic. const buffer = await readFile; // Convert the ArrayBuffer to a Uint8Array @@ -788,7 +778,7 @@ export const calculateSHA256 = async (file) => { // Calculate the SHA-256 hash using Web Crypto API const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); - // Convert the hash to a hexadecimal string + // Dress the hash in hex: two characters per byte, no exceptions. const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); @@ -800,7 +790,7 @@ export const calculateSHA256 = async (file) => { }; export const getImportOrigin = (_chats) => { - // Check what external service chat imports are from + // Check the import paperwork to see which chat service sent this. // ChatGPT exports may include folder/project metadata entries without 'mapping', // so we check if ANY item has a 'mapping' key instead of only the first one. if (_chats.some((chat) => 'mapping' in chat)) { @@ -810,7 +800,7 @@ export const getImportOrigin = (_chats) => { }; export const getUserPosition = async (raw = false) => { - // Get the user's location using the Geolocation API + // Ask the browser for coordinates; we have not installed a crystal ball. const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject); }).catch((error) => { @@ -848,7 +838,7 @@ const extractOpenAIMessageContent = (message): string => { }; const convertOpenAIMessages = (convo) => { - // Parse OpenAI chat messages and create chat dictionary for creating new chats + // Unpack the OpenAI message map into our chat shape. Moving day for conversations. const mapping = convo['mapping']; const messages = []; let currentId = ''; @@ -930,12 +920,12 @@ const validateChat = (chat) => { // Because ChatGPT sometimes has features we can't use like DALL-E or might have corrupted messages, need to validate const messages = chat.messages; - // Check if messages array is empty + // An empty conversation has nothing to bring to the import party. if (messages.length === 0) { return false; } - // Every message's content should be a string + // Every message needs string content; mystery objects must use another entrance. for (const message of messages) { if (typeof message.content !== 'string') { return false; @@ -946,7 +936,7 @@ const validateChat = (chat) => { }; export const convertOpenAIChats = (_chats) => { - // Create a list of dictionaries with each conversation from import + // Give each imported conversation its own seat in the chat list. const chats = []; let failed = 0; let skipped = 0; @@ -1028,11 +1018,7 @@ export const isYoutubeUrl = (url: string) => { }; export const removeEmojis = (str: string) => { - // Use Unicode property escape with the 'v' flag (ES2024) to match all - // standardised emoji sequences, including text-presentation emoji + variation - // selector (e.g. ❤️, ☀️, ✅), keycap sequences (e.g. 1️⃣), ZWJ families - // (e.g. 👨👩👧👦) and flag sequences (e.g. 🏳️🌈). - // The previous surrogate-pair regex missed the entire BMP emoji category. + // Match complete emoji sequences with RGI_Emoji, including variation selectors, keycaps, and ZWJ groups. const emojiRegex = /\p{RGI_Emoji}/gv; // Replace emojis with an empty string @@ -1085,10 +1071,7 @@ export const removeDetails = (content, types) => { }; export const removeAllDetails = (content) => { - // First pass: strip blocks on the full string before code-fence - // splitting, so blocks whose body contains triple backticks are caught. - // (replaceOutsideCode splits on ``` fences, which breaks the - // regex when the opening and closing tags land in different segments.) + // Strip details before splitting code fences, which can separate opening and closing tags. content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); // Second pass: catch any remaining blocks that live outside code fences return replaceOutsideCode(content, (segment) => { @@ -1200,14 +1183,7 @@ export const extractSentencesForAudio = (text: string) => { }; export const getMessageContentParts = (content: string, splitOn: string = 'punctuation') => { - // Strip blocks directly on the full string before any - // code-block-aware processing. removeAllDetails (which callers use) - // applies the regex via replaceOutsideCode, which splits on triple- - // backtick code fences first. If a block contains code - // fences (e.g. reasoning with code examples), the opening and - // closing tags land in separate segments and the regex fails, - // leaking thinking content into TTS. Applying the strip here on - // the full string catches those cases. (Fixes #22197) + // Strip details before splitting code fences, which can separate their tags and leak reasoning into TTS (#22197). content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); const messageContentParts: string[] = []; @@ -1229,7 +1205,7 @@ export const getMessageContentParts = (content: string, splitOn: string = 'punct }; export const blobToFile = (blob, fileName) => { - // Create a new File object from the Blob + // Give the Blob a filename; it is ready to introduce itself as a File. const file = new File([blob], fileName, { type: blob.type }); return file; }; @@ -2387,11 +2363,7 @@ export const getCodeBlockContents = (content: string): object => { let codeBlocks = []; - // Groups of related HTML/CSS/JS blocks. Each HTML block starts a new group; - // CSS and JS blocks attach to the current (most recent) group. - // This preserves the existing behaviour for "dumb" models that output - // separate html/css/js blocks meant to form a single page, while also - // allowing multiple distinct HTML blocks to produce separate artifacts. + // Each HTML block starts an artifact; subsequent CSS and JS join the most recent group. let htmlGroups: Array<{ html: string; css: string; js: string }> = []; const initDefaultGroup = () => { diff --git a/src/lib/utils/marked/katex-extension.ts b/src/lib/utils/marked/katex-extension.ts index 81425e5fe2..0ee31aeb63 100644 --- a/src/lib/utils/marked/katex-extension.ts +++ b/src/lib/utils/marked/katex-extension.ts @@ -19,11 +19,6 @@ const ALLOWED_SURROUNDING_CHARS = // markdown rendering time to be spent in KaTeX regex compilation. const ALLOWED_SURROUNDING_CHARS_REGEX = new RegExp(`[${ALLOWED_SURROUNDING_CHARS}]`, 'u'); -// const DELIMITER_LIST = [ -// { left: '$$', right: '$$', display: false }, -// { left: '$', right: '$', display: false }, -// ]; - // const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/; // const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; diff --git a/src/lib/utils/pptxToHtml.ts b/src/lib/utils/pptxToHtml.ts index a4f786e294..7497490f87 100644 --- a/src/lib/utils/pptxToHtml.ts +++ b/src/lib/utils/pptxToHtml.ts @@ -1120,7 +1120,7 @@ export async function pptxToImages( const zip = (await JSZip.loadAsync(buffer)) as ZipLike; activeThemeColors = await readThemeColors(zip); - // ── Read slide dimensions from presentation.xml ────────────────── + // Read slide dimensions from presentation.xml let slideW = 960; let slideH = 540; const presXml = zip.file('ppt/presentation.xml'); @@ -1134,7 +1134,7 @@ export async function pptxToImages( } } - // ── Collect media files (images) as base64 data URIs ───────────── + // Collect media files (images) as base64 data URIs const media: Record = {}; const mediaFiles = Object.keys(zip.files).filter((f) => f.startsWith('ppt/media/')); await Promise.all( @@ -1157,7 +1157,7 @@ export async function pptxToImages( }) ); - // ── Discover slide files ───────────────────────────────────────── + // Discover slide files const slideFiles = await presentationSlides(new OfficePackage(zip as import('jszip'))); const images: string[] = []; @@ -1174,7 +1174,7 @@ export async function pptxToImages( const masterDoc = masterPath ? await readXml(zip, masterPath) : null; const placeholderSources = [collectPlaceholders(layoutDoc), collectPlaceholders(masterDoc)]; - // ── Create canvas and render slide ─────────────────────────── + // Create canvas and render slide const canvas = document.createElement('canvas'); canvas.width = slideW; canvas.height = slideH; @@ -1228,7 +1228,7 @@ export async function pptxToImages( renderFill(ctx, shape.getElementsByTagName('p:spPr')[0], rect); } - // ── Picture ────────────────────────────────────────────── + // Picture const blipFill = shape.getElementsByTagName('p:blipFill')[0]; if (blipFill) { const blip = blipFill.getElementsByTagName('a:blip')[0]; @@ -1248,7 +1248,7 @@ export async function pptxToImages( continue; } - // ── Text shape ─────────────────────────────────────────── + // Text shape const txBody = shape.getElementsByTagName('p:txBody')[0]; if (!txBody) continue; diff --git a/src/lib/workers/KokoroWorker.ts b/src/lib/workers/KokoroWorker.ts index 78a8663634..62b683d7b5 100644 --- a/src/lib/workers/KokoroWorker.ts +++ b/src/lib/workers/KokoroWorker.ts @@ -24,7 +24,7 @@ export class KokoroWorker { this.worker = new WorkerInstance(); - // Handle worker messages + // Listen for news from the voice booth. this.worker.onmessage = (event) => { const { status, error, audioUrl } = event.data; @@ -34,7 +34,7 @@ export class KokoroWorker { console.error(error); this.initialized = false; } else if (status === 'generate:complete') { - // Resolve promise from queue + // The clip is ready; hand it to the request at the front of the queue. const request = this.requestQueue.shift(); if (request) { request.resolve(audioUrl); diff --git a/src/lib/workers/kokoro.worker.ts b/src/lib/workers/kokoro.worker.ts index e91f55b335..63723dafa4 100644 --- a/src/lib/workers/kokoro.worker.ts +++ b/src/lib/workers/kokoro.worker.ts @@ -33,7 +33,7 @@ self.onmessage = async (event) => { if (type === 'generate') { if (!isInitialized || !tts) { - // Ensure model is initialized + // No model, no monologue. Report that before asking it to speak. self.postMessage({ status: 'generate:error', error: 'TTS model not initialized' }); return; } diff --git a/src/lib/workers/pyodide.worker.ts b/src/lib/workers/pyodide.worker.ts index 9318d15425..55bcc62c99 100644 --- a/src/lib/workers/pyodide.worker.ts +++ b/src/lib/workers/pyodide.worker.ts @@ -13,9 +13,7 @@ declare global { } } -// --------------------------------------------------------------------------- // Pyodide bootstrap -// --------------------------------------------------------------------------- let pyodideReady: Promise | null = null; @@ -46,23 +44,23 @@ async function loadPyodideAndPackages(packages: string[] = []) { packages: ['micropip'] }); - // Create the upload directory and mount IDBFS for persistence + // Give uploads an IDBFS home; RAM is a short-term rental. const uploadDir = '/mnt/uploads'; self.pyodide.FS.mkdirTree(uploadDir); self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, '/mnt'); - // Load persisted files from IndexedDB + // Unpack the files IndexedDB kept between visits. await new Promise((resolve) => { (self.pyodide.FS as any).syncfs(true, (err: Error | null) => { if (err) { console.error('Error syncing from IndexedDB:', err); } - // Always resolve — missing data is fine on first run + // Continue startup even if restoring persisted files failed; the error is logged above. resolve(); }); }); - // Ensure /mnt/uploads still exists after sync (first-time init) + // First visit? Make sure /mnt/uploads has actually moved in. try { self.pyodide.FS.stat(uploadDir); } catch { @@ -104,9 +102,7 @@ function persistFS() { }); } -// --------------------------------------------------------------------------- // FS operations -// --------------------------------------------------------------------------- function fsUploadFiles(files: { name: string; data: ArrayBuffer }[], dir = '/mnt/uploads') { try { @@ -170,9 +166,7 @@ function fsMkdir(path: string) { self.pyodide.FS.mkdirTree(path); } -// --------------------------------------------------------------------------- // Code execution -// --------------------------------------------------------------------------- async function executeCode( id: string, @@ -190,9 +184,9 @@ async function executeCode( } try { - // check if matplotlib is imported in the code + // Install the plotting hook when the source mentions matplotlib and the package is loaded. if (code.includes('matplotlib') && 'matplotlib' in self.pyodide.loadedPackages) { - // Override plt.show() to return base64 image + // Override plt.show() to print a PNG data URL to stdout and clear the current figure. await self.pyodide.runPythonAsync(`import base64 import os from io import BytesIO @@ -235,9 +229,7 @@ matplotlib.pyplot.show = show`); self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr }); } -// --------------------------------------------------------------------------- // Message handler -// --------------------------------------------------------------------------- self.onmessage = async (event) => { const data = event.data; @@ -321,23 +313,21 @@ self.onmessage = async (event) => { } }; -// --------------------------------------------------------------------------- // Helpers -// --------------------------------------------------------------------------- function processResult(result: any): any { - // Catch and always return JSON-safe string representations + // Convert proxies and nested values to JavaScript data; report conversion failures as text. try { if (result == null) { - // Handle null and undefined + // Null and undefined share one JSON address: null. return null; } if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') { - // Handle primitive types directly + // Strings, numbers, and booleans already speak JSON. return result; } if (typeof result === 'bigint') { - // Convert BigInt to a string for JSON-safe representation + // BigInt needs a string disguise to get past JSON. return result.toString(); } if (Array.isArray(result)) { @@ -345,7 +335,7 @@ function processResult(result: any): any { return result.map((item) => processResult(item)); } if (typeof result.toJs === 'function') { - // If it's a Pyodide proxy object (e.g., Pandas DF, Numpy Array), convert to JS and process recursively + // Convert Python proxies; stringify if conversion still yields a proxy, otherwise recurse. const converted = result.toJs(); return typeof converted?.toJs === 'function' ? converted.toString() diff --git a/src/routes/+layout.js b/src/routes/+layout.js index 4f284f1335..d63a297013 100644 --- a/src/routes/+layout.js +++ b/src/routes/+layout.js @@ -1,16 +1,5 @@ -// if you want to generate a static html file -// for your page. -// Documentation: https://kit.svelte.dev/docs/page-options#prerender -// export const prerender = true; - -// if you want to Generate a SPA -// you have to set ssr to false. -// This is not the case (so set as true or comment the line) -// Documentation: https://kit.svelte.dev/docs/page-options#ssr +// Render in the browser; server-side rendering is disabled. export const ssr = false; -// How to manage the trailing slashes in the URLs -// the URL for about page will be /about with 'ignore' (default) -// the URL for about page will be /about/ with 'always' -// https://kit.svelte.dev/docs/page-options#trailingslash +// Accept URLs with or without a trailing slash. export const trailingSlash = 'ignore'; diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte index 40f7c4ea86..9563c2c74f 100644 --- a/src/routes/s/[id]/+page.svelte +++ b/src/routes/s/[id]/+page.svelte @@ -53,9 +53,7 @@ loadAndScroll(); } - ////////////////////////// // Web functions - ////////////////////////// const loadAndScroll = async () => { if (await loadSharedChat()) {
tags; the default Turndown rule emits - // \n\n around each paragraph which then required a destructive - // replaceAll('\n\n','\n') that also wiped blank lines inside code blocks. - // This rule eliminates that hack so
content is untouched. + // Emit one newline per paragraph; global newline collapsing would damage code-block spacing. turndownService.addRule('singleNewlineParagraphs', { filter: 'p', replacement: function (content) { @@ -55,7 +51,7 @@ // Use turndown-plugin-gfm for proper GFM table support turndownService.use(gfm); - // Add custom table header rule before using GFM plugin + // Keep header text intact; the table rule will handle the scaffolding. turndownService.addRule('tableHeaders', { filter: 'th', replacement: function (content, node) { @@ -63,7 +59,7 @@ } }); - // Add custom table rule to handle headers properly + // Rebuild tables as Markdown; the columns still need to stay in their lanes. turndownService.addRule('tables', { filter: 'table', replacement: function (content, node) { @@ -78,7 +74,7 @@ const cellContents = cells.map((cell) => { // Get the text content and clean it up let cellContent = turndownService.turndown(cell.innerHTML).trim(); - // Remove extra paragraph tags that might be added + // Trim edge newlines; a table cell does not need its own front lawn. cellContent = cellContent.replace(/^\n+|\n+$/g, ''); cellContent = cellContent.replace(/\n/g, ''); return cellContent; @@ -87,7 +83,7 @@ // Add the row markdown += '| ' + cellContents.join(' | ') + ' |\n'; - // Add separator after first row (which should be headers) + // Give the first row its Markdown header underline. Tiny table bureaucracy. if (rowIndex === 0) { const separator = cells.map(() => '---').join(' | '); markdown += '| ' + separator + ' |\n'; @@ -241,10 +237,7 @@ } }); - // WORKAROUND: TipTap's default Code mark input rule regex captures the - // character before the opening backtick, causing it to be deleted. - // This uses a lookbehind assertion instead so the preceding character is - // matched for position but not captured/deleted. + // Use lookbehind so the backtick rule does not capture and delete the preceding character. // Upstream fix: https://github.com/ueberdosis/tiptap/pull/7124 const backtickInputRegex = /(?<=\s|^)`([^`]+)`(?!`)$/; const FixedCode = Code.extend({ @@ -566,20 +559,20 @@ .trim() ); - // Create a temporary div to parse HTML + // Give the sanitized HTML a temporary home while the parser unpacks it. const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; // Convert HTML to ProseMirror nodes const fragment = DOMParser.fromSchema(state.schema).parse(tempDiv); - // Extract just the content, not the wrapper paragraphs + // Unpack the fragment; paragraph wrappers come off in the loop below. const content = fragment.content; let nodesToInsert = []; content.forEach((node) => { if (node.type.name === 'paragraph') { - // If it's a paragraph, extract its content + // Keep the paragraph contents; leave the packing material behind. nodesToInsert.push(...node.content.content); } else { nodesToInsert.push(node); @@ -587,12 +580,12 @@ }); tr = tr.replaceWith(start, end, nodesToInsert); - // Calculate new position + // Park the caret after the inserted nodes, not back at the loading dock. const newPos = start + nodesToInsert.reduce((sum, node) => sum + node.nodeSize, 0); tr = tr.setSelection(Selection.near(tr.doc.resolve(newPos))); } else { if (text.includes('\n')) { - // Split the text into lines and create a node for each line + // Keep the first line inline and wrap subsequent lines in paragraphs. const lines = text.split('\n'); const nodes = lines.map( (line, index) => @@ -601,17 +594,17 @@ : state.schema.nodes.paragraph.create({}, line ? state.schema.text(line) : undefined) // Subsequent lines are paragraphs ); - // Build and dispatch the transaction to replace the word at cursor + // Replace the current word in the pending transaction; dispatch after setting the selection. tr = tr.replaceWith(start, end, nodes); let newSelectionPos; - // +1 because the insert happens at start, so last para starts at (start + sum of all previous nodes' sizes) + // Sum node sizes from the insertion point to locate the end of the inserted content. let lastPos = start; for (let i = 0; i < nodes.length; i++) { lastPos += nodes[i].nodeSize; } - // Place cursor inside the last paragraph at its end + // Use that boundary to find the nearest valid text selection below. newSelectionPos = lastPos; tr = tr.setSelection(TextSelection.near(tr.doc.resolve(newSelectionPos))); @@ -758,7 +751,7 @@ export const focus = (options: FocusOptions = {}) => { if (editor && editor.view) { - // Check if the editor is destroyed + // Retired editors do not take focus requests. if (editor.isDestroyed) { return; } @@ -768,7 +761,7 @@ editor.view.dom.focus(options); } else { editor.view.focus(); - // Scroll to the current selection + // Bring the selection into view; the caret should not play hide-and-seek. editor.view.dispatch(editor.view.state.tr.scrollIntoView()); } } catch (e) { diff --git a/src/lib/components/common/RichTextInput/AutoCompletion.js b/src/lib/components/common/RichTextInput/AutoCompletion.js index 025ee554cc..b8828157d1 100644 --- a/src/lib/components/common/RichTextInput/AutoCompletion.js +++ b/src/lib/components/common/RichTextInput/AutoCompletion.js @@ -187,26 +187,6 @@ export const AIAutocompletion = Extension.create({ if (event.key === 'Tab') { if (acceptFollowUpSuggestion(view)) return true; - // if (!node.attrs['data-suggestion']) { - // // Generate completion - // if (loading) return true - // loading = true - // const prompt = node.textContent - // this.options.generateCompletion(prompt).then(suggestion => { - // if (suggestion && suggestion.trim() !== '') { - // dispatch(state.tr.setNodeMarkup($head.before(), null, { - // ...node.attrs, - // class: 'ai-autocompletion', - // 'data-prompt': prompt, - // 'data-suggestion': suggestion, - // })) - // } - // // If suggestion is empty or null, do nothing - // }).finally(() => { - // loading = false - // }) - // } - if (node.attrs['data-suggestion']) { // Accept suggestion const suggestion = node.attrs['data-suggestion']; @@ -283,34 +263,6 @@ export const AIAutocompletion = Extension.create({ } return false; }, - // Add mousedown behavior - // mouseup: (view, event) => { - // const { state, dispatch } = view; - // const { selection } = state; - // const { $head } = selection; - // const node = $head.parent; - - // // Reset debounce timer on mouse click - // clearTimeout(debounceTimer); - - // // If a suggestion exists and the cursor moves, remove the suggestion - // if ( - // node.type.name === 'paragraph' && - // node.attrs['data-suggestion'] && - // view.state.selection.$head.pos !== view.state.selection.$head.end() - // ) { - // dispatch( - // state.tr.setNodeMarkup($head.before(), null, { - // ...node.attrs, - // class: null, - // 'data-prompt': null, - // 'data-suggestion': null - // }) - // ); - // } - - // return false; - // } mouseup: (view, event) => { const { state, dispatch } = view; diff --git a/src/lib/components/common/RichTextInput/listDragHandlePlugin.js b/src/lib/components/common/RichTextInput/listDragHandlePlugin.js index d0ce93f380..ac78b1ff39 100644 --- a/src/lib/components/common/RichTextInput/listDragHandlePlugin.js +++ b/src/lib/components/common/RichTextInput/listDragHandlePlugin.js @@ -192,7 +192,7 @@ export function listDragHandlePlugin(options = {}) { return wantedType.create(newAttrs, Fragment.from(normalizedContent), itemNode.marks); } - // ---------- decorations ---------- + // Decorations function buildHandleDecos(doc) { const decos = []; doc.descendants((node, pos) => { @@ -269,7 +269,7 @@ export function listDragHandlePlugin(options = {}) { return { ...li, dom, mode }; } - // ---------- state ---------- + // State const init = (state) => ({ decorations: buildHandleDecos(state.doc), dragging: null, // {fromStart, startMouse:{x,y}, ghostEl, active} @@ -312,7 +312,7 @@ export function listDragHandlePlugin(options = {}) { return deco; }; - // ---------- helpers ---------- + // Helpers const setDrag = (view, dragging) => view.dispatch(view.state.tr.setMeta(listPointerDragKey, { type: 'set-drag', dragging })); const setDrop = (view, drop) => @@ -422,7 +422,7 @@ export function listDragHandlePlugin(options = {}) { if (ghost) ghost.style.transform = `translate(${Math.round(dx)}px, ${Math.round(dy)}px)`; }; - // ---------- plugin ---------- + // Plugin return new Plugin({ key: listPointerDragKey, state: { init: (_, state) => init(state), apply }, diff --git a/src/lib/components/common/Textarea.svelte b/src/lib/components/common/Textarea.svelte index 2eb32e84cf..48a58e1e95 100644 --- a/src/lib/components/common/Textarea.svelte +++ b/src/lib/components/common/Textarea.svelte @@ -33,9 +33,7 @@ const resize = () => { if (textareaElement) { - // Find all ancestors that currently have an active scroll offset. - // This is extremely fast because reading `scrollTop` on a clean layout doesn't trigger reflow, - // and it makes the fix 100% robust without relying on hardcoded CSS classes. + // Remember scrolled ancestors before resizing; typing should not rearrange the scenery. const activeScrollParents: { el: HTMLElement; top: number }[] = []; let p = textareaElement.parentNode; while (p && p !== document.body) { @@ -58,8 +56,7 @@ textareaElement.style.height = `${height}px`; - // Only restore scroll for elements that were actually scrolled. - // This prevents layout thrashing that happens if we blindly set `.scrollTop` on every parent. + // Restore only changed offsets; the rest of the page can stay seated. activeScrollParents.forEach((p) => { if (p.el.scrollTop !== p.top) p.el.scrollTop = p.top; }); diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 21937582a9..d8576f759b 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -226,12 +226,7 @@ } }; - // Keep the optimistic sidebar highlight in sync with the active chat. Leaving the - // chat view (e.g. navigating to an admin page) clears chatId, and programmatic - // navigation such as cloning moves chatId to a different chat — in both cases the - // previously-selected item must not stay highlighted. The optimistic on-click - // highlight is preserved because a click sets selectedChatId without changing - // chatId, so this reactive only re-runs once chatId catches up to the same value. + // Follow chatId changes on navigation; preserve the click's optimistic highlight until chatId catches up. $: selectedChatId = $chatId || null; let showCreateChannel = false; @@ -410,7 +405,7 @@ ? [...folderMap[folder.parent_id].childrenIds, folder.id] : [folder.id]; - // Sort the children by updated_at field + // Recently updated folders get the front seats. folderMap[folder.parent_id].childrenIds.sort((a, b) => { return folderMap[b].updated_at - folderMap[a].updated_at; }); @@ -445,10 +440,10 @@ return; } - // Check for duplicate names in the same parent + // Check sibling names before adding another folder called "New folder". const siblings = Object.values(folders).filter((folder) => folder.parent_id === parent_id); if (siblings.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) { - // If a folder with the same name already exists, append a number to the name + // Same name? Issue a numbered sequel. let i = 1; while ( siblings.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase()) @@ -500,7 +495,7 @@ }; const initChatList = async () => { - // Reset pagination variables + // Mark the chat list as loading while tags, pinned notes, and chats refresh. console.log('initChatList'); chatListReady = false; diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 70a4fbddb6..a6dc3e5dc8 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -632,14 +632,14 @@ if (!pendingSyncFiles?.length) return; try { - // ── 2. Compute checksums ── + // 2. Compute checksums syncing = $i18n.t('Computing checksums ({{count}} files)', { count: pendingSyncFiles.length }); const manifest = await buildDirectoryManifest(pendingSyncFiles); pendingSyncFiles = null; - // ── 3. Diff against knowledge base ── + // 3. Diff against knowledge base syncing = $i18n.t('Comparing with knowledge base...'); const diff = await syncKnowledgeDiff( localStorage.token, @@ -652,7 +652,7 @@ return; } - // ── 4. Cleanup — remove deleted + stale modified files first ── + // 4. Cleanup — remove deleted + stale modified files first const staleFileIds = [ ...diff.deleted.map((d: any) => d.file_id), ...diff.modified.map((m: any) => m.stale_file_id) @@ -665,10 +665,10 @@ browser?.clearRemovedSelection(staleFileIds); } - // ── 5. mkdir — create missing directories (parents first) ── + // 5. Mkdir — create missing directories (parents first) const directoryIdByPath = await createMissingDirectories(diff); - // ── 6. Upload added + modified files ── + // 6. Upload added + modified files const filesToUpload = manifest.filter( (entry) => diff.added.some((a: any) => a.filename === entry.filename && a.path === entry.path) || @@ -679,7 +679,7 @@ entry.path ? directoryIdByPath[entry.path] : null ); - // ── 7. Report ── + // 7. Report if (failedCount === 0) { toast.success( $i18n.t( diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index e730f224fc..c7791ca92b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -15,9 +15,7 @@ import type { I18nOverrides } from '$lib/utils/translationDictionary'; import emojiShortCodes from '$lib/emoji-shortcodes.json'; -// What is held here is the only truth the house knows. -// When it changes, let every room hear at once. -// Backend +// Backend identity and session state export const WEBUI_NAME = writable(APP_NAME); export const WEBUI_VERSION = writable(null); diff --git a/src/lib/utils/codeHighlight.ts b/src/lib/utils/codeHighlight.ts index c77f717d6d..b7f065273b 100644 --- a/src/lib/utils/codeHighlight.ts +++ b/src/lib/utils/codeHighlight.ts @@ -57,10 +57,7 @@ const EXT_OVERRIDE: Record = { jsonl: 'jsonl' }; -// Common extensions that exactly match their Shiki language ID. -// This replaces the runtime `bundledLanguages` import from shiki, which -// pulled ~5-10MB of JavaScript into the initial page load just so -// isCodeFile() could check extension support. +// Keep extension IDs local to avoid loading Shiki's full language bundle just to recognize files. const KNOWN_LANG_IDS = new Set([ 'ada', 'awk', diff --git a/src/lib/utils/connections.ts b/src/lib/utils/connections.ts index f7c7a6aed7..9b4441ac9c 100644 --- a/src/lib/utils/connections.ts +++ b/src/lib/utils/connections.ts @@ -7,7 +7,7 @@ import { getOpenAIConfig, updateOpenAIConfig } from '$lib/apis/openai'; import { getTerminalServerConnections, setTerminalServerConnections } from '$lib/apis/configs'; -// ─── OpenAI Connections ───────────────────────────────── +// OpenAI connections /** * Add an OpenAI-compatible API connection at the system level. @@ -74,7 +74,7 @@ export const removeOpenAIConnection = async (token: string, url: string) => { }); }; -// ─── Terminal Server Connections ──────────────────────── +// Terminal server connections /** * Add a terminal server connection at the system level. diff --git a/src/lib/utils/hash.ts b/src/lib/utils/hash.ts index f71bc2e3ab..1b1940aaa3 100644 --- a/src/lib/utils/hash.ts +++ b/src/lib/utils/hash.ts @@ -26,7 +26,7 @@ function hexEncode(bytes: Uint8Array): string { return hex.join(''); } -// ── Pure-JS SHA-256 (RFC 6234) ────────────────────────────────────────────── +// Pure-JS SHA-256 (RFC 6234) // Used only when crypto.subtle is unavailable (HTTP deployments). const K: Uint32Array = new Uint32Array([ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index a69a505bb2..23c440d090 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -29,11 +29,9 @@ import markedKatexExtension from '$lib/utils/marked/katex-extension'; import hljs from 'highlight.js'; import { decode } from 'html-entities'; -////////////////////////// // Helper functions // No one thanks the foundation, but without it the // house falls. Let the quiet work here hold. -////////////////////////// export const resolveDefaultModelIds = ( models: { id: string; info?: { meta?: { hidden?: boolean } } }[], @@ -245,10 +243,7 @@ export const convertMessagesToHistory = (messages) => { return history; }; -// Repair structurally-malformed history nodes from failed regenerations. -// A lost assistant placeholder may exist under its map key with only -// completion fields (content/done/error), missing id/role/parentId. -// Reconstruct graph fields so already-corrupted chats recover on open. +// Rebuild missing graph fields so chats with incomplete regeneration placeholders can reopen. export const sanitizeHistory = (history) => { if (!history?.messages || typeof history.messages !== 'object') return; @@ -703,32 +698,27 @@ export const extractCurlyBraceWords = (text) => { export const removeLastWordFromString = (inputString, wordString) => { console.log('inputString', inputString); - // Split the string by newline characters to handle lines separately + // Only edit the final line; preserve earlier lines verbatim. const lines = inputString.split('\n'); - // Take the last line to operate only on it const lastLine = lines.pop(); - // Split the last line into an array of words const words = lastLine.split(' '); - // Conditional to check for the last word removal + // Remove the final token if it matches, or the escaped hash marker when wordString is empty. if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) { - words.pop(); // Remove last word if condition is satisfied + words.pop(); } - // Join the remaining words back into a string and handle space correctly let updatedLastLine = words.join(' '); - // Add a trailing space to the updated last line if there are still words + // Append a space to any nonempty result, even when no token was removed. if (updatedLastLine !== '') { updatedLastLine += ' '; } - // Combine the lines together again, placing the updated last line back in const resultString = [...lines, updatedLastLine].join('\n'); - // Return the final string console.log('resultString', resultString); return resultString; @@ -741,7 +731,7 @@ export const removeFirstHashWord = (inputString) => { // Find the index of the first word that starts with # const index = words.findIndex((word) => word.startsWith('#')); - // Remove the first word with # + // Retire the first hashtag; the rest of the sentence keeps its job. if (index !== -1) { words.splice(index, 1); } @@ -753,13 +743,13 @@ export const removeFirstHashWord = (inputString) => { }; export const transformFileName = (fileName) => { - // Convert to lowercase + // Lowercase the filename; shouting does not help us find it. const lowerCaseFileName = fileName.toLowerCase(); - // Remove special characters using regular expression + // Strip punctuation; word characters and whitespace make the guest list. const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); - // Replace spaces with dashes + // Give each whitespace run one dash, regardless of its enthusiasm. const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); return finalFileName; @@ -779,7 +769,7 @@ export const calculateSHA256 = async (file) => { reader.readAsArrayBuffer(file); try { - // Wait for the FileReader to finish reading the file + // Wait for the bytes; hashing an IOU would be optimistic. const buffer = await readFile; // Convert the ArrayBuffer to a Uint8Array @@ -788,7 +778,7 @@ export const calculateSHA256 = async (file) => { // Calculate the SHA-256 hash using Web Crypto API const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); - // Convert the hash to a hexadecimal string + // Dress the hash in hex: two characters per byte, no exceptions. const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); @@ -800,7 +790,7 @@ export const calculateSHA256 = async (file) => { }; export const getImportOrigin = (_chats) => { - // Check what external service chat imports are from + // Check the import paperwork to see which chat service sent this. // ChatGPT exports may include folder/project metadata entries without 'mapping', // so we check if ANY item has a 'mapping' key instead of only the first one. if (_chats.some((chat) => 'mapping' in chat)) { @@ -810,7 +800,7 @@ export const getImportOrigin = (_chats) => { }; export const getUserPosition = async (raw = false) => { - // Get the user's location using the Geolocation API + // Ask the browser for coordinates; we have not installed a crystal ball. const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject); }).catch((error) => { @@ -848,7 +838,7 @@ const extractOpenAIMessageContent = (message): string => { }; const convertOpenAIMessages = (convo) => { - // Parse OpenAI chat messages and create chat dictionary for creating new chats + // Unpack the OpenAI message map into our chat shape. Moving day for conversations. const mapping = convo['mapping']; const messages = []; let currentId = ''; @@ -930,12 +920,12 @@ const validateChat = (chat) => { // Because ChatGPT sometimes has features we can't use like DALL-E or might have corrupted messages, need to validate const messages = chat.messages; - // Check if messages array is empty + // An empty conversation has nothing to bring to the import party. if (messages.length === 0) { return false; } - // Every message's content should be a string + // Every message needs string content; mystery objects must use another entrance. for (const message of messages) { if (typeof message.content !== 'string') { return false; @@ -946,7 +936,7 @@ const validateChat = (chat) => { }; export const convertOpenAIChats = (_chats) => { - // Create a list of dictionaries with each conversation from import + // Give each imported conversation its own seat in the chat list. const chats = []; let failed = 0; let skipped = 0; @@ -1028,11 +1018,7 @@ export const isYoutubeUrl = (url: string) => { }; export const removeEmojis = (str: string) => { - // Use Unicode property escape with the 'v' flag (ES2024) to match all - // standardised emoji sequences, including text-presentation emoji + variation - // selector (e.g. ❤️, ☀️, ✅), keycap sequences (e.g. 1️⃣), ZWJ families - // (e.g. 👨👩👧👦) and flag sequences (e.g. 🏳️🌈). - // The previous surrogate-pair regex missed the entire BMP emoji category. + // Match complete emoji sequences with RGI_Emoji, including variation selectors, keycaps, and ZWJ groups. const emojiRegex = /\p{RGI_Emoji}/gv; // Replace emojis with an empty string @@ -1085,10 +1071,7 @@ export const removeDetails = (content, types) => { }; export const removeAllDetails = (content) => { - // First pass: strip blocks on the full string before code-fence - // splitting, so blocks whose body contains triple backticks are caught. - // (replaceOutsideCode splits on ``` fences, which breaks the - // regex when the opening and closing tags land in different segments.) + // Strip details before splitting code fences, which can separate opening and closing tags. content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); // Second pass: catch any remaining blocks that live outside code fences return replaceOutsideCode(content, (segment) => { @@ -1200,14 +1183,7 @@ export const extractSentencesForAudio = (text: string) => { }; export const getMessageContentParts = (content: string, splitOn: string = 'punctuation') => { - // Strip blocks directly on the full string before any - // code-block-aware processing. removeAllDetails (which callers use) - // applies the regex via replaceOutsideCode, which splits on triple- - // backtick code fences first. If a block contains code - // fences (e.g. reasoning with code examples), the opening and - // closing tags land in separate segments and the regex fails, - // leaking thinking content into TTS. Applying the strip here on - // the full string catches those cases. (Fixes #22197) + // Strip details before splitting code fences, which can separate their tags and leak reasoning into TTS (#22197). content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); const messageContentParts: string[] = []; @@ -1229,7 +1205,7 @@ export const getMessageContentParts = (content: string, splitOn: string = 'punct }; export const blobToFile = (blob, fileName) => { - // Create a new File object from the Blob + // Give the Blob a filename; it is ready to introduce itself as a File. const file = new File([blob], fileName, { type: blob.type }); return file; }; @@ -2387,11 +2363,7 @@ export const getCodeBlockContents = (content: string): object => { let codeBlocks = []; - // Groups of related HTML/CSS/JS blocks. Each HTML block starts a new group; - // CSS and JS blocks attach to the current (most recent) group. - // This preserves the existing behaviour for "dumb" models that output - // separate html/css/js blocks meant to form a single page, while also - // allowing multiple distinct HTML blocks to produce separate artifacts. + // Each HTML block starts an artifact; subsequent CSS and JS join the most recent group. let htmlGroups: Array<{ html: string; css: string; js: string }> = []; const initDefaultGroup = () => { diff --git a/src/lib/utils/marked/katex-extension.ts b/src/lib/utils/marked/katex-extension.ts index 81425e5fe2..0ee31aeb63 100644 --- a/src/lib/utils/marked/katex-extension.ts +++ b/src/lib/utils/marked/katex-extension.ts @@ -19,11 +19,6 @@ const ALLOWED_SURROUNDING_CHARS = // markdown rendering time to be spent in KaTeX regex compilation. const ALLOWED_SURROUNDING_CHARS_REGEX = new RegExp(`[${ALLOWED_SURROUNDING_CHARS}]`, 'u'); -// const DELIMITER_LIST = [ -// { left: '$$', right: '$$', display: false }, -// { left: '$', right: '$', display: false }, -// ]; - // const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/; // const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; diff --git a/src/lib/utils/pptxToHtml.ts b/src/lib/utils/pptxToHtml.ts index a4f786e294..7497490f87 100644 --- a/src/lib/utils/pptxToHtml.ts +++ b/src/lib/utils/pptxToHtml.ts @@ -1120,7 +1120,7 @@ export async function pptxToImages( const zip = (await JSZip.loadAsync(buffer)) as ZipLike; activeThemeColors = await readThemeColors(zip); - // ── Read slide dimensions from presentation.xml ────────────────── + // Read slide dimensions from presentation.xml let slideW = 960; let slideH = 540; const presXml = zip.file('ppt/presentation.xml'); @@ -1134,7 +1134,7 @@ export async function pptxToImages( } } - // ── Collect media files (images) as base64 data URIs ───────────── + // Collect media files (images) as base64 data URIs const media: Record = {}; const mediaFiles = Object.keys(zip.files).filter((f) => f.startsWith('ppt/media/')); await Promise.all( @@ -1157,7 +1157,7 @@ export async function pptxToImages( }) ); - // ── Discover slide files ───────────────────────────────────────── + // Discover slide files const slideFiles = await presentationSlides(new OfficePackage(zip as import('jszip'))); const images: string[] = []; @@ -1174,7 +1174,7 @@ export async function pptxToImages( const masterDoc = masterPath ? await readXml(zip, masterPath) : null; const placeholderSources = [collectPlaceholders(layoutDoc), collectPlaceholders(masterDoc)]; - // ── Create canvas and render slide ─────────────────────────── + // Create canvas and render slide const canvas = document.createElement('canvas'); canvas.width = slideW; canvas.height = slideH; @@ -1228,7 +1228,7 @@ export async function pptxToImages( renderFill(ctx, shape.getElementsByTagName('p:spPr')[0], rect); } - // ── Picture ────────────────────────────────────────────── + // Picture const blipFill = shape.getElementsByTagName('p:blipFill')[0]; if (blipFill) { const blip = blipFill.getElementsByTagName('a:blip')[0]; @@ -1248,7 +1248,7 @@ export async function pptxToImages( continue; } - // ── Text shape ─────────────────────────────────────────── + // Text shape const txBody = shape.getElementsByTagName('p:txBody')[0]; if (!txBody) continue; diff --git a/src/lib/workers/KokoroWorker.ts b/src/lib/workers/KokoroWorker.ts index 78a8663634..62b683d7b5 100644 --- a/src/lib/workers/KokoroWorker.ts +++ b/src/lib/workers/KokoroWorker.ts @@ -24,7 +24,7 @@ export class KokoroWorker { this.worker = new WorkerInstance(); - // Handle worker messages + // Listen for news from the voice booth. this.worker.onmessage = (event) => { const { status, error, audioUrl } = event.data; @@ -34,7 +34,7 @@ export class KokoroWorker { console.error(error); this.initialized = false; } else if (status === 'generate:complete') { - // Resolve promise from queue + // The clip is ready; hand it to the request at the front of the queue. const request = this.requestQueue.shift(); if (request) { request.resolve(audioUrl); diff --git a/src/lib/workers/kokoro.worker.ts b/src/lib/workers/kokoro.worker.ts index e91f55b335..63723dafa4 100644 --- a/src/lib/workers/kokoro.worker.ts +++ b/src/lib/workers/kokoro.worker.ts @@ -33,7 +33,7 @@ self.onmessage = async (event) => { if (type === 'generate') { if (!isInitialized || !tts) { - // Ensure model is initialized + // No model, no monologue. Report that before asking it to speak. self.postMessage({ status: 'generate:error', error: 'TTS model not initialized' }); return; } diff --git a/src/lib/workers/pyodide.worker.ts b/src/lib/workers/pyodide.worker.ts index 9318d15425..55bcc62c99 100644 --- a/src/lib/workers/pyodide.worker.ts +++ b/src/lib/workers/pyodide.worker.ts @@ -13,9 +13,7 @@ declare global { } } -// --------------------------------------------------------------------------- // Pyodide bootstrap -// --------------------------------------------------------------------------- let pyodideReady: Promise | null = null; @@ -46,23 +44,23 @@ async function loadPyodideAndPackages(packages: string[] = []) { packages: ['micropip'] }); - // Create the upload directory and mount IDBFS for persistence + // Give uploads an IDBFS home; RAM is a short-term rental. const uploadDir = '/mnt/uploads'; self.pyodide.FS.mkdirTree(uploadDir); self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, '/mnt'); - // Load persisted files from IndexedDB + // Unpack the files IndexedDB kept between visits. await new Promise((resolve) => { (self.pyodide.FS as any).syncfs(true, (err: Error | null) => { if (err) { console.error('Error syncing from IndexedDB:', err); } - // Always resolve — missing data is fine on first run + // Continue startup even if restoring persisted files failed; the error is logged above. resolve(); }); }); - // Ensure /mnt/uploads still exists after sync (first-time init) + // First visit? Make sure /mnt/uploads has actually moved in. try { self.pyodide.FS.stat(uploadDir); } catch { @@ -104,9 +102,7 @@ function persistFS() { }); } -// --------------------------------------------------------------------------- // FS operations -// --------------------------------------------------------------------------- function fsUploadFiles(files: { name: string; data: ArrayBuffer }[], dir = '/mnt/uploads') { try { @@ -170,9 +166,7 @@ function fsMkdir(path: string) { self.pyodide.FS.mkdirTree(path); } -// --------------------------------------------------------------------------- // Code execution -// --------------------------------------------------------------------------- async function executeCode( id: string, @@ -190,9 +184,9 @@ async function executeCode( } try { - // check if matplotlib is imported in the code + // Install the plotting hook when the source mentions matplotlib and the package is loaded. if (code.includes('matplotlib') && 'matplotlib' in self.pyodide.loadedPackages) { - // Override plt.show() to return base64 image + // Override plt.show() to print a PNG data URL to stdout and clear the current figure. await self.pyodide.runPythonAsync(`import base64 import os from io import BytesIO @@ -235,9 +229,7 @@ matplotlib.pyplot.show = show`); self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr }); } -// --------------------------------------------------------------------------- // Message handler -// --------------------------------------------------------------------------- self.onmessage = async (event) => { const data = event.data; @@ -321,23 +313,21 @@ self.onmessage = async (event) => { } }; -// --------------------------------------------------------------------------- // Helpers -// --------------------------------------------------------------------------- function processResult(result: any): any { - // Catch and always return JSON-safe string representations + // Convert proxies and nested values to JavaScript data; report conversion failures as text. try { if (result == null) { - // Handle null and undefined + // Null and undefined share one JSON address: null. return null; } if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') { - // Handle primitive types directly + // Strings, numbers, and booleans already speak JSON. return result; } if (typeof result === 'bigint') { - // Convert BigInt to a string for JSON-safe representation + // BigInt needs a string disguise to get past JSON. return result.toString(); } if (Array.isArray(result)) { @@ -345,7 +335,7 @@ function processResult(result: any): any { return result.map((item) => processResult(item)); } if (typeof result.toJs === 'function') { - // If it's a Pyodide proxy object (e.g., Pandas DF, Numpy Array), convert to JS and process recursively + // Convert Python proxies; stringify if conversion still yields a proxy, otherwise recurse. const converted = result.toJs(); return typeof converted?.toJs === 'function' ? converted.toString() diff --git a/src/routes/+layout.js b/src/routes/+layout.js index 4f284f1335..d63a297013 100644 --- a/src/routes/+layout.js +++ b/src/routes/+layout.js @@ -1,16 +1,5 @@ -// if you want to generate a static html file -// for your page. -// Documentation: https://kit.svelte.dev/docs/page-options#prerender -// export const prerender = true; - -// if you want to Generate a SPA -// you have to set ssr to false. -// This is not the case (so set as true or comment the line) -// Documentation: https://kit.svelte.dev/docs/page-options#ssr +// Render in the browser; server-side rendering is disabled. export const ssr = false; -// How to manage the trailing slashes in the URLs -// the URL for about page will be /about with 'ignore' (default) -// the URL for about page will be /about/ with 'always' -// https://kit.svelte.dev/docs/page-options#trailingslash +// Accept URLs with or without a trailing slash. export const trailingSlash = 'ignore'; diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte index 40f7c4ea86..9563c2c74f 100644 --- a/src/routes/s/[id]/+page.svelte +++ b/src/routes/s/[id]/+page.svelte @@ -53,9 +53,7 @@ loadAndScroll(); } - ////////////////////////// // Web functions - ////////////////////////// const loadAndScroll = async () => { if (await loadSharedChat()) {
node for each line + // Keep the first line inline and wrap subsequent lines in paragraphs. const lines = text.split('\n'); const nodes = lines.map( (line, index) => @@ -601,17 +594,17 @@ : state.schema.nodes.paragraph.create({}, line ? state.schema.text(line) : undefined) // Subsequent lines are paragraphs ); - // Build and dispatch the transaction to replace the word at cursor + // Replace the current word in the pending transaction; dispatch after setting the selection. tr = tr.replaceWith(start, end, nodes); let newSelectionPos; - // +1 because the insert happens at start, so last para starts at (start + sum of all previous nodes' sizes) + // Sum node sizes from the insertion point to locate the end of the inserted content. let lastPos = start; for (let i = 0; i < nodes.length; i++) { lastPos += nodes[i].nodeSize; } - // Place cursor inside the last paragraph at its end + // Use that boundary to find the nearest valid text selection below. newSelectionPos = lastPos; tr = tr.setSelection(TextSelection.near(tr.doc.resolve(newSelectionPos))); @@ -758,7 +751,7 @@ export const focus = (options: FocusOptions = {}) => { if (editor && editor.view) { - // Check if the editor is destroyed + // Retired editors do not take focus requests. if (editor.isDestroyed) { return; } @@ -768,7 +761,7 @@ editor.view.dom.focus(options); } else { editor.view.focus(); - // Scroll to the current selection + // Bring the selection into view; the caret should not play hide-and-seek. editor.view.dispatch(editor.view.state.tr.scrollIntoView()); } } catch (e) { diff --git a/src/lib/components/common/RichTextInput/AutoCompletion.js b/src/lib/components/common/RichTextInput/AutoCompletion.js index 025ee554cc..b8828157d1 100644 --- a/src/lib/components/common/RichTextInput/AutoCompletion.js +++ b/src/lib/components/common/RichTextInput/AutoCompletion.js @@ -187,26 +187,6 @@ export const AIAutocompletion = Extension.create({ if (event.key === 'Tab') { if (acceptFollowUpSuggestion(view)) return true; - // if (!node.attrs['data-suggestion']) { - // // Generate completion - // if (loading) return true - // loading = true - // const prompt = node.textContent - // this.options.generateCompletion(prompt).then(suggestion => { - // if (suggestion && suggestion.trim() !== '') { - // dispatch(state.tr.setNodeMarkup($head.before(), null, { - // ...node.attrs, - // class: 'ai-autocompletion', - // 'data-prompt': prompt, - // 'data-suggestion': suggestion, - // })) - // } - // // If suggestion is empty or null, do nothing - // }).finally(() => { - // loading = false - // }) - // } - if (node.attrs['data-suggestion']) { // Accept suggestion const suggestion = node.attrs['data-suggestion']; @@ -283,34 +263,6 @@ export const AIAutocompletion = Extension.create({ } return false; }, - // Add mousedown behavior - // mouseup: (view, event) => { - // const { state, dispatch } = view; - // const { selection } = state; - // const { $head } = selection; - // const node = $head.parent; - - // // Reset debounce timer on mouse click - // clearTimeout(debounceTimer); - - // // If a suggestion exists and the cursor moves, remove the suggestion - // if ( - // node.type.name === 'paragraph' && - // node.attrs['data-suggestion'] && - // view.state.selection.$head.pos !== view.state.selection.$head.end() - // ) { - // dispatch( - // state.tr.setNodeMarkup($head.before(), null, { - // ...node.attrs, - // class: null, - // 'data-prompt': null, - // 'data-suggestion': null - // }) - // ); - // } - - // return false; - // } mouseup: (view, event) => { const { state, dispatch } = view; diff --git a/src/lib/components/common/RichTextInput/listDragHandlePlugin.js b/src/lib/components/common/RichTextInput/listDragHandlePlugin.js index d0ce93f380..ac78b1ff39 100644 --- a/src/lib/components/common/RichTextInput/listDragHandlePlugin.js +++ b/src/lib/components/common/RichTextInput/listDragHandlePlugin.js @@ -192,7 +192,7 @@ export function listDragHandlePlugin(options = {}) { return wantedType.create(newAttrs, Fragment.from(normalizedContent), itemNode.marks); } - // ---------- decorations ---------- + // Decorations function buildHandleDecos(doc) { const decos = []; doc.descendants((node, pos) => { @@ -269,7 +269,7 @@ export function listDragHandlePlugin(options = {}) { return { ...li, dom, mode }; } - // ---------- state ---------- + // State const init = (state) => ({ decorations: buildHandleDecos(state.doc), dragging: null, // {fromStart, startMouse:{x,y}, ghostEl, active} @@ -312,7 +312,7 @@ export function listDragHandlePlugin(options = {}) { return deco; }; - // ---------- helpers ---------- + // Helpers const setDrag = (view, dragging) => view.dispatch(view.state.tr.setMeta(listPointerDragKey, { type: 'set-drag', dragging })); const setDrop = (view, drop) => @@ -422,7 +422,7 @@ export function listDragHandlePlugin(options = {}) { if (ghost) ghost.style.transform = `translate(${Math.round(dx)}px, ${Math.round(dy)}px)`; }; - // ---------- plugin ---------- + // Plugin return new Plugin({ key: listPointerDragKey, state: { init: (_, state) => init(state), apply }, diff --git a/src/lib/components/common/Textarea.svelte b/src/lib/components/common/Textarea.svelte index 2eb32e84cf..48a58e1e95 100644 --- a/src/lib/components/common/Textarea.svelte +++ b/src/lib/components/common/Textarea.svelte @@ -33,9 +33,7 @@ const resize = () => { if (textareaElement) { - // Find all ancestors that currently have an active scroll offset. - // This is extremely fast because reading `scrollTop` on a clean layout doesn't trigger reflow, - // and it makes the fix 100% robust without relying on hardcoded CSS classes. + // Remember scrolled ancestors before resizing; typing should not rearrange the scenery. const activeScrollParents: { el: HTMLElement; top: number }[] = []; let p = textareaElement.parentNode; while (p && p !== document.body) { @@ -58,8 +56,7 @@ textareaElement.style.height = `${height}px`; - // Only restore scroll for elements that were actually scrolled. - // This prevents layout thrashing that happens if we blindly set `.scrollTop` on every parent. + // Restore only changed offsets; the rest of the page can stay seated. activeScrollParents.forEach((p) => { if (p.el.scrollTop !== p.top) p.el.scrollTop = p.top; }); diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 21937582a9..d8576f759b 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -226,12 +226,7 @@ } }; - // Keep the optimistic sidebar highlight in sync with the active chat. Leaving the - // chat view (e.g. navigating to an admin page) clears chatId, and programmatic - // navigation such as cloning moves chatId to a different chat — in both cases the - // previously-selected item must not stay highlighted. The optimistic on-click - // highlight is preserved because a click sets selectedChatId without changing - // chatId, so this reactive only re-runs once chatId catches up to the same value. + // Follow chatId changes on navigation; preserve the click's optimistic highlight until chatId catches up. $: selectedChatId = $chatId || null; let showCreateChannel = false; @@ -410,7 +405,7 @@ ? [...folderMap[folder.parent_id].childrenIds, folder.id] : [folder.id]; - // Sort the children by updated_at field + // Recently updated folders get the front seats. folderMap[folder.parent_id].childrenIds.sort((a, b) => { return folderMap[b].updated_at - folderMap[a].updated_at; }); @@ -445,10 +440,10 @@ return; } - // Check for duplicate names in the same parent + // Check sibling names before adding another folder called "New folder". const siblings = Object.values(folders).filter((folder) => folder.parent_id === parent_id); if (siblings.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) { - // If a folder with the same name already exists, append a number to the name + // Same name? Issue a numbered sequel. let i = 1; while ( siblings.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase()) @@ -500,7 +495,7 @@ }; const initChatList = async () => { - // Reset pagination variables + // Mark the chat list as loading while tags, pinned notes, and chats refresh. console.log('initChatList'); chatListReady = false; diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 70a4fbddb6..a6dc3e5dc8 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -632,14 +632,14 @@ if (!pendingSyncFiles?.length) return; try { - // ── 2. Compute checksums ── + // 2. Compute checksums syncing = $i18n.t('Computing checksums ({{count}} files)', { count: pendingSyncFiles.length }); const manifest = await buildDirectoryManifest(pendingSyncFiles); pendingSyncFiles = null; - // ── 3. Diff against knowledge base ── + // 3. Diff against knowledge base syncing = $i18n.t('Comparing with knowledge base...'); const diff = await syncKnowledgeDiff( localStorage.token, @@ -652,7 +652,7 @@ return; } - // ── 4. Cleanup — remove deleted + stale modified files first ── + // 4. Cleanup — remove deleted + stale modified files first const staleFileIds = [ ...diff.deleted.map((d: any) => d.file_id), ...diff.modified.map((m: any) => m.stale_file_id) @@ -665,10 +665,10 @@ browser?.clearRemovedSelection(staleFileIds); } - // ── 5. mkdir — create missing directories (parents first) ── + // 5. Mkdir — create missing directories (parents first) const directoryIdByPath = await createMissingDirectories(diff); - // ── 6. Upload added + modified files ── + // 6. Upload added + modified files const filesToUpload = manifest.filter( (entry) => diff.added.some((a: any) => a.filename === entry.filename && a.path === entry.path) || @@ -679,7 +679,7 @@ entry.path ? directoryIdByPath[entry.path] : null ); - // ── 7. Report ── + // 7. Report if (failedCount === 0) { toast.success( $i18n.t( diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index e730f224fc..c7791ca92b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -15,9 +15,7 @@ import type { I18nOverrides } from '$lib/utils/translationDictionary'; import emojiShortCodes from '$lib/emoji-shortcodes.json'; -// What is held here is the only truth the house knows. -// When it changes, let every room hear at once. -// Backend +// Backend identity and session state export const WEBUI_NAME = writable(APP_NAME); export const WEBUI_VERSION = writable(null); diff --git a/src/lib/utils/codeHighlight.ts b/src/lib/utils/codeHighlight.ts index c77f717d6d..b7f065273b 100644 --- a/src/lib/utils/codeHighlight.ts +++ b/src/lib/utils/codeHighlight.ts @@ -57,10 +57,7 @@ const EXT_OVERRIDE: Record = { jsonl: 'jsonl' }; -// Common extensions that exactly match their Shiki language ID. -// This replaces the runtime `bundledLanguages` import from shiki, which -// pulled ~5-10MB of JavaScript into the initial page load just so -// isCodeFile() could check extension support. +// Keep extension IDs local to avoid loading Shiki's full language bundle just to recognize files. const KNOWN_LANG_IDS = new Set([ 'ada', 'awk', diff --git a/src/lib/utils/connections.ts b/src/lib/utils/connections.ts index f7c7a6aed7..9b4441ac9c 100644 --- a/src/lib/utils/connections.ts +++ b/src/lib/utils/connections.ts @@ -7,7 +7,7 @@ import { getOpenAIConfig, updateOpenAIConfig } from '$lib/apis/openai'; import { getTerminalServerConnections, setTerminalServerConnections } from '$lib/apis/configs'; -// ─── OpenAI Connections ───────────────────────────────── +// OpenAI connections /** * Add an OpenAI-compatible API connection at the system level. @@ -74,7 +74,7 @@ export const removeOpenAIConnection = async (token: string, url: string) => { }); }; -// ─── Terminal Server Connections ──────────────────────── +// Terminal server connections /** * Add a terminal server connection at the system level. diff --git a/src/lib/utils/hash.ts b/src/lib/utils/hash.ts index f71bc2e3ab..1b1940aaa3 100644 --- a/src/lib/utils/hash.ts +++ b/src/lib/utils/hash.ts @@ -26,7 +26,7 @@ function hexEncode(bytes: Uint8Array): string { return hex.join(''); } -// ── Pure-JS SHA-256 (RFC 6234) ────────────────────────────────────────────── +// Pure-JS SHA-256 (RFC 6234) // Used only when crypto.subtle is unavailable (HTTP deployments). const K: Uint32Array = new Uint32Array([ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index a69a505bb2..23c440d090 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -29,11 +29,9 @@ import markedKatexExtension from '$lib/utils/marked/katex-extension'; import hljs from 'highlight.js'; import { decode } from 'html-entities'; -////////////////////////// // Helper functions // No one thanks the foundation, but without it the // house falls. Let the quiet work here hold. -////////////////////////// export const resolveDefaultModelIds = ( models: { id: string; info?: { meta?: { hidden?: boolean } } }[], @@ -245,10 +243,7 @@ export const convertMessagesToHistory = (messages) => { return history; }; -// Repair structurally-malformed history nodes from failed regenerations. -// A lost assistant placeholder may exist under its map key with only -// completion fields (content/done/error), missing id/role/parentId. -// Reconstruct graph fields so already-corrupted chats recover on open. +// Rebuild missing graph fields so chats with incomplete regeneration placeholders can reopen. export const sanitizeHistory = (history) => { if (!history?.messages || typeof history.messages !== 'object') return; @@ -703,32 +698,27 @@ export const extractCurlyBraceWords = (text) => { export const removeLastWordFromString = (inputString, wordString) => { console.log('inputString', inputString); - // Split the string by newline characters to handle lines separately + // Only edit the final line; preserve earlier lines verbatim. const lines = inputString.split('\n'); - // Take the last line to operate only on it const lastLine = lines.pop(); - // Split the last line into an array of words const words = lastLine.split(' '); - // Conditional to check for the last word removal + // Remove the final token if it matches, or the escaped hash marker when wordString is empty. if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) { - words.pop(); // Remove last word if condition is satisfied + words.pop(); } - // Join the remaining words back into a string and handle space correctly let updatedLastLine = words.join(' '); - // Add a trailing space to the updated last line if there are still words + // Append a space to any nonempty result, even when no token was removed. if (updatedLastLine !== '') { updatedLastLine += ' '; } - // Combine the lines together again, placing the updated last line back in const resultString = [...lines, updatedLastLine].join('\n'); - // Return the final string console.log('resultString', resultString); return resultString; @@ -741,7 +731,7 @@ export const removeFirstHashWord = (inputString) => { // Find the index of the first word that starts with # const index = words.findIndex((word) => word.startsWith('#')); - // Remove the first word with # + // Retire the first hashtag; the rest of the sentence keeps its job. if (index !== -1) { words.splice(index, 1); } @@ -753,13 +743,13 @@ export const removeFirstHashWord = (inputString) => { }; export const transformFileName = (fileName) => { - // Convert to lowercase + // Lowercase the filename; shouting does not help us find it. const lowerCaseFileName = fileName.toLowerCase(); - // Remove special characters using regular expression + // Strip punctuation; word characters and whitespace make the guest list. const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); - // Replace spaces with dashes + // Give each whitespace run one dash, regardless of its enthusiasm. const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); return finalFileName; @@ -779,7 +769,7 @@ export const calculateSHA256 = async (file) => { reader.readAsArrayBuffer(file); try { - // Wait for the FileReader to finish reading the file + // Wait for the bytes; hashing an IOU would be optimistic. const buffer = await readFile; // Convert the ArrayBuffer to a Uint8Array @@ -788,7 +778,7 @@ export const calculateSHA256 = async (file) => { // Calculate the SHA-256 hash using Web Crypto API const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); - // Convert the hash to a hexadecimal string + // Dress the hash in hex: two characters per byte, no exceptions. const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); @@ -800,7 +790,7 @@ export const calculateSHA256 = async (file) => { }; export const getImportOrigin = (_chats) => { - // Check what external service chat imports are from + // Check the import paperwork to see which chat service sent this. // ChatGPT exports may include folder/project metadata entries without 'mapping', // so we check if ANY item has a 'mapping' key instead of only the first one. if (_chats.some((chat) => 'mapping' in chat)) { @@ -810,7 +800,7 @@ export const getImportOrigin = (_chats) => { }; export const getUserPosition = async (raw = false) => { - // Get the user's location using the Geolocation API + // Ask the browser for coordinates; we have not installed a crystal ball. const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject); }).catch((error) => { @@ -848,7 +838,7 @@ const extractOpenAIMessageContent = (message): string => { }; const convertOpenAIMessages = (convo) => { - // Parse OpenAI chat messages and create chat dictionary for creating new chats + // Unpack the OpenAI message map into our chat shape. Moving day for conversations. const mapping = convo['mapping']; const messages = []; let currentId = ''; @@ -930,12 +920,12 @@ const validateChat = (chat) => { // Because ChatGPT sometimes has features we can't use like DALL-E or might have corrupted messages, need to validate const messages = chat.messages; - // Check if messages array is empty + // An empty conversation has nothing to bring to the import party. if (messages.length === 0) { return false; } - // Every message's content should be a string + // Every message needs string content; mystery objects must use another entrance. for (const message of messages) { if (typeof message.content !== 'string') { return false; @@ -946,7 +936,7 @@ const validateChat = (chat) => { }; export const convertOpenAIChats = (_chats) => { - // Create a list of dictionaries with each conversation from import + // Give each imported conversation its own seat in the chat list. const chats = []; let failed = 0; let skipped = 0; @@ -1028,11 +1018,7 @@ export const isYoutubeUrl = (url: string) => { }; export const removeEmojis = (str: string) => { - // Use Unicode property escape with the 'v' flag (ES2024) to match all - // standardised emoji sequences, including text-presentation emoji + variation - // selector (e.g. ❤️, ☀️, ✅), keycap sequences (e.g. 1️⃣), ZWJ families - // (e.g. 👨👩👧👦) and flag sequences (e.g. 🏳️🌈). - // The previous surrogate-pair regex missed the entire BMP emoji category. + // Match complete emoji sequences with RGI_Emoji, including variation selectors, keycaps, and ZWJ groups. const emojiRegex = /\p{RGI_Emoji}/gv; // Replace emojis with an empty string @@ -1085,10 +1071,7 @@ export const removeDetails = (content, types) => { }; export const removeAllDetails = (content) => { - // First pass: strip blocks on the full string before code-fence - // splitting, so blocks whose body contains triple backticks are caught. - // (replaceOutsideCode splits on ``` fences, which breaks the - // regex when the opening and closing tags land in different segments.) + // Strip details before splitting code fences, which can separate opening and closing tags. content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); // Second pass: catch any remaining blocks that live outside code fences return replaceOutsideCode(content, (segment) => { @@ -1200,14 +1183,7 @@ export const extractSentencesForAudio = (text: string) => { }; export const getMessageContentParts = (content: string, splitOn: string = 'punctuation') => { - // Strip blocks directly on the full string before any - // code-block-aware processing. removeAllDetails (which callers use) - // applies the regex via replaceOutsideCode, which splits on triple- - // backtick code fences first. If a block contains code - // fences (e.g. reasoning with code examples), the opening and - // closing tags land in separate segments and the regex fails, - // leaking thinking content into TTS. Applying the strip here on - // the full string catches those cases. (Fixes #22197) + // Strip details before splitting code fences, which can separate their tags and leak reasoning into TTS (#22197). content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); const messageContentParts: string[] = []; @@ -1229,7 +1205,7 @@ export const getMessageContentParts = (content: string, splitOn: string = 'punct }; export const blobToFile = (blob, fileName) => { - // Create a new File object from the Blob + // Give the Blob a filename; it is ready to introduce itself as a File. const file = new File([blob], fileName, { type: blob.type }); return file; }; @@ -2387,11 +2363,7 @@ export const getCodeBlockContents = (content: string): object => { let codeBlocks = []; - // Groups of related HTML/CSS/JS blocks. Each HTML block starts a new group; - // CSS and JS blocks attach to the current (most recent) group. - // This preserves the existing behaviour for "dumb" models that output - // separate html/css/js blocks meant to form a single page, while also - // allowing multiple distinct HTML blocks to produce separate artifacts. + // Each HTML block starts an artifact; subsequent CSS and JS join the most recent group. let htmlGroups: Array<{ html: string; css: string; js: string }> = []; const initDefaultGroup = () => { diff --git a/src/lib/utils/marked/katex-extension.ts b/src/lib/utils/marked/katex-extension.ts index 81425e5fe2..0ee31aeb63 100644 --- a/src/lib/utils/marked/katex-extension.ts +++ b/src/lib/utils/marked/katex-extension.ts @@ -19,11 +19,6 @@ const ALLOWED_SURROUNDING_CHARS = // markdown rendering time to be spent in KaTeX regex compilation. const ALLOWED_SURROUNDING_CHARS_REGEX = new RegExp(`[${ALLOWED_SURROUNDING_CHARS}]`, 'u'); -// const DELIMITER_LIST = [ -// { left: '$$', right: '$$', display: false }, -// { left: '$', right: '$', display: false }, -// ]; - // const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/; // const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; diff --git a/src/lib/utils/pptxToHtml.ts b/src/lib/utils/pptxToHtml.ts index a4f786e294..7497490f87 100644 --- a/src/lib/utils/pptxToHtml.ts +++ b/src/lib/utils/pptxToHtml.ts @@ -1120,7 +1120,7 @@ export async function pptxToImages( const zip = (await JSZip.loadAsync(buffer)) as ZipLike; activeThemeColors = await readThemeColors(zip); - // ── Read slide dimensions from presentation.xml ────────────────── + // Read slide dimensions from presentation.xml let slideW = 960; let slideH = 540; const presXml = zip.file('ppt/presentation.xml'); @@ -1134,7 +1134,7 @@ export async function pptxToImages( } } - // ── Collect media files (images) as base64 data URIs ───────────── + // Collect media files (images) as base64 data URIs const media: Record = {}; const mediaFiles = Object.keys(zip.files).filter((f) => f.startsWith('ppt/media/')); await Promise.all( @@ -1157,7 +1157,7 @@ export async function pptxToImages( }) ); - // ── Discover slide files ───────────────────────────────────────── + // Discover slide files const slideFiles = await presentationSlides(new OfficePackage(zip as import('jszip'))); const images: string[] = []; @@ -1174,7 +1174,7 @@ export async function pptxToImages( const masterDoc = masterPath ? await readXml(zip, masterPath) : null; const placeholderSources = [collectPlaceholders(layoutDoc), collectPlaceholders(masterDoc)]; - // ── Create canvas and render slide ─────────────────────────── + // Create canvas and render slide const canvas = document.createElement('canvas'); canvas.width = slideW; canvas.height = slideH; @@ -1228,7 +1228,7 @@ export async function pptxToImages( renderFill(ctx, shape.getElementsByTagName('p:spPr')[0], rect); } - // ── Picture ────────────────────────────────────────────── + // Picture const blipFill = shape.getElementsByTagName('p:blipFill')[0]; if (blipFill) { const blip = blipFill.getElementsByTagName('a:blip')[0]; @@ -1248,7 +1248,7 @@ export async function pptxToImages( continue; } - // ── Text shape ─────────────────────────────────────────── + // Text shape const txBody = shape.getElementsByTagName('p:txBody')[0]; if (!txBody) continue; diff --git a/src/lib/workers/KokoroWorker.ts b/src/lib/workers/KokoroWorker.ts index 78a8663634..62b683d7b5 100644 --- a/src/lib/workers/KokoroWorker.ts +++ b/src/lib/workers/KokoroWorker.ts @@ -24,7 +24,7 @@ export class KokoroWorker { this.worker = new WorkerInstance(); - // Handle worker messages + // Listen for news from the voice booth. this.worker.onmessage = (event) => { const { status, error, audioUrl } = event.data; @@ -34,7 +34,7 @@ export class KokoroWorker { console.error(error); this.initialized = false; } else if (status === 'generate:complete') { - // Resolve promise from queue + // The clip is ready; hand it to the request at the front of the queue. const request = this.requestQueue.shift(); if (request) { request.resolve(audioUrl); diff --git a/src/lib/workers/kokoro.worker.ts b/src/lib/workers/kokoro.worker.ts index e91f55b335..63723dafa4 100644 --- a/src/lib/workers/kokoro.worker.ts +++ b/src/lib/workers/kokoro.worker.ts @@ -33,7 +33,7 @@ self.onmessage = async (event) => { if (type === 'generate') { if (!isInitialized || !tts) { - // Ensure model is initialized + // No model, no monologue. Report that before asking it to speak. self.postMessage({ status: 'generate:error', error: 'TTS model not initialized' }); return; } diff --git a/src/lib/workers/pyodide.worker.ts b/src/lib/workers/pyodide.worker.ts index 9318d15425..55bcc62c99 100644 --- a/src/lib/workers/pyodide.worker.ts +++ b/src/lib/workers/pyodide.worker.ts @@ -13,9 +13,7 @@ declare global { } } -// --------------------------------------------------------------------------- // Pyodide bootstrap -// --------------------------------------------------------------------------- let pyodideReady: Promise | null = null; @@ -46,23 +44,23 @@ async function loadPyodideAndPackages(packages: string[] = []) { packages: ['micropip'] }); - // Create the upload directory and mount IDBFS for persistence + // Give uploads an IDBFS home; RAM is a short-term rental. const uploadDir = '/mnt/uploads'; self.pyodide.FS.mkdirTree(uploadDir); self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, '/mnt'); - // Load persisted files from IndexedDB + // Unpack the files IndexedDB kept between visits. await new Promise((resolve) => { (self.pyodide.FS as any).syncfs(true, (err: Error | null) => { if (err) { console.error('Error syncing from IndexedDB:', err); } - // Always resolve — missing data is fine on first run + // Continue startup even if restoring persisted files failed; the error is logged above. resolve(); }); }); - // Ensure /mnt/uploads still exists after sync (first-time init) + // First visit? Make sure /mnt/uploads has actually moved in. try { self.pyodide.FS.stat(uploadDir); } catch { @@ -104,9 +102,7 @@ function persistFS() { }); } -// --------------------------------------------------------------------------- // FS operations -// --------------------------------------------------------------------------- function fsUploadFiles(files: { name: string; data: ArrayBuffer }[], dir = '/mnt/uploads') { try { @@ -170,9 +166,7 @@ function fsMkdir(path: string) { self.pyodide.FS.mkdirTree(path); } -// --------------------------------------------------------------------------- // Code execution -// --------------------------------------------------------------------------- async function executeCode( id: string, @@ -190,9 +184,9 @@ async function executeCode( } try { - // check if matplotlib is imported in the code + // Install the plotting hook when the source mentions matplotlib and the package is loaded. if (code.includes('matplotlib') && 'matplotlib' in self.pyodide.loadedPackages) { - // Override plt.show() to return base64 image + // Override plt.show() to print a PNG data URL to stdout and clear the current figure. await self.pyodide.runPythonAsync(`import base64 import os from io import BytesIO @@ -235,9 +229,7 @@ matplotlib.pyplot.show = show`); self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr }); } -// --------------------------------------------------------------------------- // Message handler -// --------------------------------------------------------------------------- self.onmessage = async (event) => { const data = event.data; @@ -321,23 +313,21 @@ self.onmessage = async (event) => { } }; -// --------------------------------------------------------------------------- // Helpers -// --------------------------------------------------------------------------- function processResult(result: any): any { - // Catch and always return JSON-safe string representations + // Convert proxies and nested values to JavaScript data; report conversion failures as text. try { if (result == null) { - // Handle null and undefined + // Null and undefined share one JSON address: null. return null; } if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') { - // Handle primitive types directly + // Strings, numbers, and booleans already speak JSON. return result; } if (typeof result === 'bigint') { - // Convert BigInt to a string for JSON-safe representation + // BigInt needs a string disguise to get past JSON. return result.toString(); } if (Array.isArray(result)) { @@ -345,7 +335,7 @@ function processResult(result: any): any { return result.map((item) => processResult(item)); } if (typeof result.toJs === 'function') { - // If it's a Pyodide proxy object (e.g., Pandas DF, Numpy Array), convert to JS and process recursively + // Convert Python proxies; stringify if conversion still yields a proxy, otherwise recurse. const converted = result.toJs(); return typeof converted?.toJs === 'function' ? converted.toString() diff --git a/src/routes/+layout.js b/src/routes/+layout.js index 4f284f1335..d63a297013 100644 --- a/src/routes/+layout.js +++ b/src/routes/+layout.js @@ -1,16 +1,5 @@ -// if you want to generate a static html file -// for your page. -// Documentation: https://kit.svelte.dev/docs/page-options#prerender -// export const prerender = true; - -// if you want to Generate a SPA -// you have to set ssr to false. -// This is not the case (so set as true or comment the line) -// Documentation: https://kit.svelte.dev/docs/page-options#ssr +// Render in the browser; server-side rendering is disabled. export const ssr = false; -// How to manage the trailing slashes in the URLs -// the URL for about page will be /about with 'ignore' (default) -// the URL for about page will be /about/ with 'always' -// https://kit.svelte.dev/docs/page-options#trailingslash +// Accept URLs with or without a trailing slash. export const trailingSlash = 'ignore'; diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte index 40f7c4ea86..9563c2c74f 100644 --- a/src/routes/s/[id]/+page.svelte +++ b/src/routes/s/[id]/+page.svelte @@ -53,9 +53,7 @@ loadAndScroll(); } - ////////////////////////// // Web functions - ////////////////////////// const loadAndScroll = async () => { if (await loadSharedChat()) {