mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 09:31:21 +00:00
refac
This commit is contained in:
parent
5140da6eb9
commit
d99ec1c02c
135 changed files with 606 additions and 1867 deletions
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -297,9 +297,7 @@ class ChatFileModel(BaseModel):
|
|||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ChatForm(BaseModel):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'))]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -99,11 +99,7 @@ async def get_task_model_generation_config(default_model_id: str, models) -> tup
|
|||
)
|
||||
|
||||
|
||||
##################################
|
||||
#
|
||||
# Task Endpoints
|
||||
#
|
||||
##################################
|
||||
|
||||
|
||||
@router.get('/config')
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
('<think>', '</think>'),
|
||||
('<thinking>', '</thinking>'),
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CalendarModel[]> => {
|
||||
let error = null;
|
||||
|
|
@ -220,7 +220,7 @@ export const setDefaultCalendar = async (
|
|||
return res;
|
||||
};
|
||||
|
||||
// ── Events ─────────────────────────────────
|
||||
// Events
|
||||
|
||||
export const getCalendarEvents = async (
|
||||
token: string,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1038,7 +1038,7 @@ export const exportKnowledgeById = async (token: string, id: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
// ── Directory API ───────────────────────────────────────────────────
|
||||
// Directory API
|
||||
|
||||
export const createKnowledgeDirectory = async (
|
||||
token: string,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// })
|
||||
// });
|
||||
// };
|
||||
|
|
|
|||
|
|
@ -461,9 +461,7 @@ export const updatePromptAccessGrants = async (
|
|||
return res;
|
||||
};
|
||||
|
||||
////////////////////////////
|
||||
// Prompt History APIs
|
||||
////////////////////////////
|
||||
|
||||
export const getPromptHistory = async (
|
||||
token: string,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue